# Data Anonymization and Privacy Compliance **Last updated:** 2026-02 **Status:** GA **Category:** Data Engineering for AI --- ## Introduksjon Personvern og databeskyttelse er fundamentale krav for enhver AI-losning som behandler personopplysninger. GDPR (og den norske Personopplysningsloven) stiller strenge krav til hvordan persondata samles inn, behandles og beskyttes. For AI-systemer er dette spesielt utfordrende: ML-modeller kan utilsiktet memorere persondata fra treningsdatasettet, og RAG-systemer kan eksponere sensitiv informasjon i svar. Microsoft tilbyr flere verktoy for personvernbeskyttelse: Azure Language PII-deteksjon for a identifisere og maskere personopplysninger, Microsoft Purview for dataklassifisering og governance, og SmartNoise for differensiell personvern. Disse verktoyene kan integreres i datapipelines i Fabric for a sikre at AI-modeller trenes pa korrekt anonymiserte data. For norsk offentlig sektor, som er underlagt bade GDPR, Personopplysningsloven og sektorspesifikke krav (f.eks. Helseregisterloven, Politiregisterloven), er systematisk anonymisering og personvernbeskyttelse ikke bare god praksis -- det er lovpalagt. --- ## Differential Privacy Techniques ### Hva er differensiell personvern? Differensiell personvern (DP) garanterer matematisk at ingen enkeltperson kan identifiseres fra resultatet av en dataanalyse. Prinsippet: tilforing av kontrollert stoy gjor det umulig a avgjore om en spesifikk person er i datasettet. ``` Formell definisjon: For alle datasett D1 og D2 som skiller seg med maks 1 rad, og alle mulige resultater S: Pr[M(D1) i S] <= e^epsilon * Pr[M(D2) i S] epsilon (privacy budget): Lavere = sterkere personvern, mer stoy ``` ### Privacy Budget (epsilon) | Epsilon | Personvernniva | Bruksomrade | |---------|---------------|-------------| | 0.1 | Svart sterkt | Helsedata, sensitiv forskning | | 1.0 | Sterkt | Generell offentlig statistikk | | 3.0 | Moderat | Intern analyse, dashboards | | 10.0 | Svakt | Testing, lav-risiko data | ### SmartNoise-implementering ```python # SmartNoise - Microsoft/OpenDP-prosjektet for differensiell personvern # pip install opendp smartnoise-sql from opendp.measurements import make_laplace from opendp.domains import atom_domain from opendp.metrics import absolute_distance def dp_count(true_count: int, epsilon: float = 1.0) -> float: """ Legg til Laplace-stoy for differensielt privat telling. """ sensitivity = 1 # En person kan endre tellingen med maks 1 scale = sensitivity / epsilon import numpy as np noise = np.random.laplace(0, scale) return max(0, true_count + noise) # Aldri negativ telling def dp_mean(values, epsilon: float = 1.0, lower_bound: float = 0, upper_bound: float = 100): """ Beregn differensielt privat gjennomsnitt. """ import numpy as np n = len(values) true_mean = np.mean(values) sensitivity = (upper_bound - lower_bound) / n scale = sensitivity / epsilon noise = np.random.laplace(0, scale) return true_mean + noise # Eksempel: Privat gjennomsnitt av inntektsdata incomes = [450000, 520000, 380000, 620000, 490000] private_mean = dp_mean(incomes, epsilon=1.0, lower_bound=0, upper_bound=2000000) print(f"Differensielt privat gjennomsnitt: {private_mean:,.0f} NOK") ``` ### Differensiell personvern i ML-trening ```python # DP-SGD (Differentially Private Stochastic Gradient Descent) # For trening av modeller med personverngarantier # Med opacus (PyTorch) # pip install opacus """ from opacus import PrivacyEngine model = YourModel() optimizer = torch.optim.SGD(model.parameters(), lr=0.01) privacy_engine = PrivacyEngine() model, optimizer, dataloader = privacy_engine.make_private_with_epsilon( module=model, optimizer=optimizer, data_loader=dataloader, epochs=10, target_epsilon=3.0, # Privacy budget target_delta=1e-5, max_grad_norm=1.0 ) # Tren som vanlig - opacus handterer stoy-tilforing automatisk for epoch in range(10): for batch in dataloader: optimizer.zero_grad() loss = criterion(model(batch), labels) loss.backward() optimizer.step() # Sjekk faktisk privacy-forbruk epsilon = privacy_engine.get_epsilon(delta=1e-5) print(f"Faktisk epsilon: {epsilon:.2f}") """ ``` --- ## K-Anonymity and L-Diversity ### K-Anonymitet K-anonymitet sikrer at hver kombinasjon av quasi-identifikatorer forekommer i minst k rader: ```python def check_k_anonymity(df, quasi_identifiers: list, k: int = 5) -> dict: """ Sjekk om et datasett oppfyller k-anonymitet. Args: df: DataFrame quasi_identifiers: Kolonner som kan brukes til re-identifisering k: Minimum gruppestorrelse """ # Grupper etter quasi-identifikatorer groups = df.groupBy(quasi_identifiers).count() # Finn grupper med faerre enn k elementer violating = groups.filter(F.col("count") < k) total_groups = groups.count() violating_groups = violating.count() min_group_size = groups.agg(F.min("count")).collect()[0][0] return { "k_anonymous": violating_groups == 0, "k_value": k, "total_groups": total_groups, "violating_groups": violating_groups, "min_group_size": min_group_size, "recommendation": f"Oek generalisering" if min_group_size < k else "OK" } # Sjekk k-anonymitet for helsedatasett result = check_k_anonymity( df_health_data, quasi_identifiers=["age_group", "postal_area", "gender"], k=5 ) ``` ### Generaliseringsstrategier for k-anonymitet ```python def generalize_for_k_anonymity(df, generalizations: dict): """ Generaliser quasi-identifikatorer for a oppna k-anonymitet. Args: generalizations: {kolonne: generaliseringsfunksjon} """ result = df for col_name, gen_func in generalizations.items(): result = result.withColumn(col_name, gen_func(F.col(col_name))) return result # Generaliseringsfunksjoner generalizations = { # Alder -> aldersgruppe (5-aarsintervaller) "age": lambda c: (F.floor(c / 5) * 5).cast("int"), # Postnummer -> postomrade (forste 2 siffer) "postal_code": lambda c: F.substring(c, 1, 2), # Fodselsdato -> fodeselsar "birth_date": lambda c: F.year(c), # Kommune -> fylke "municipality": lambda c: F.substring(c, 1, 2) # Forste 2 siffer = fylke } df_generalized = generalize_for_k_anonymity(df_sensitive, generalizations) # Verifiser result = check_k_anonymity(df_generalized, ["age", "postal_code", "gender"], k=5) print(f"K-anonym: {result['k_anonymous']}, Min gruppestorrelse: {result['min_group_size']}") ``` ### L-Diversitet L-diversitet utvider k-anonymitet ved a kreve at sensitive attributter har minst l forskjellige verdier i hver gruppe: ```python def check_l_diversity(df, quasi_identifiers: list, sensitive_column: str, l: int = 3): """ Sjekk om et datasett oppfyller l-diversitet. """ # Tell unike verdier av sensitiv attributt per gruppe diversity = df.groupBy(quasi_identifiers).agg( F.countDistinct(sensitive_column).alias("distinct_sensitive"), F.count("*").alias("group_size") ) violating = diversity.filter(F.col("distinct_sensitive") < l) min_diversity = diversity.agg(F.min("distinct_sensitive")).collect()[0][0] return { "l_diverse": violating.count() == 0, "l_value": l, "min_diversity": min_diversity, "violating_groups": violating.count() } # Sjekk l-diversitet for diagnosekoder result = check_l_diversity( df_health_data, quasi_identifiers=["age_group", "postal_area"], sensitive_column="diagnosis_code", l=3 ) ``` --- ## PII Detection and Masking ### Azure Language PII Detection Azure Language tilbyr avansert PII-deteksjon med stotte for 50+ kategorier: ```python from azure.ai.textanalytics import TextAnalyticsClient from azure.core.credentials import AzureKeyCredential def detect_and_redact_pii(text: str, endpoint: str, key: str, categories_to_redact: list = None) -> dict: """ Detekter og masker PII i tekst med Azure Language. Args: text: Tekst a analysere categories_to_redact: Spesifikke PII-kategorier a maskere """ client = TextAnalyticsClient( endpoint=endpoint, credential=AzureKeyCredential(key) ) response = client.recognize_pii_entities( documents=[text], categories_filter=categories_to_redact, language="no" # Norsk ) result = response[0] return { "original_text": text, "redacted_text": result.redacted_text, "entities": [ { "text": entity.text, "category": entity.category, "subcategory": entity.subcategory, "confidence": entity.confidence_score, "offset": entity.offset, "length": entity.length } for entity in result.entities ] } # Eksempel result = detect_and_redact_pii( text="Ola Nordmann bor i Storgata 15, 0184 Oslo. Hans personnummer er 01019012345.", endpoint="https://myservice.cognitiveservices.azure.com/", key="your-api-key", categories_to_redact=["Person", "Address", "NorwayIdentityNumber"] ) # Output: "***** bor i *****, ***** Oslo. Hans personnummer er *****." ``` ### PII-deteksjon i Fabric-pipelines ```python # Batch PII-deteksjon i PySpark from pyspark.sql import functions as F from pyspark.sql.types import ArrayType, StructType, StructField, StringType, DoubleType def batch_pii_detection(df, text_column: str, endpoint: str, key: str): """ Kjor PII-deteksjon pa en hel DataFrame-kolonne. """ @F.udf(returnType=StringType()) def redact_pii_udf(text): if not text: return text from azure.ai.textanalytics import TextAnalyticsClient from azure.core.credentials import AzureKeyCredential client = TextAnalyticsClient( endpoint=endpoint, credential=AzureKeyCredential(key) ) try: response = client.recognize_pii_entities( documents=[text], language="no" ) return response[0].redacted_text except Exception: return text # Returner original ved feil return df.withColumn(f"{text_column}_redacted", redact_pii_udf(F.col(text_column))) # Bruk pa treningsdata for RAG df_documents = spark.table("lakehouse.default.raw_documents") df_redacted = batch_pii_detection( df_documents, text_column="content", endpoint=endpoint, key=api_key ) # Lagre redaktert versjon for AI-trening df_redacted.select("doc_id", "content_redacted", "metadata") \ .write.format("delta").mode("overwrite") \ .saveAsTable("lakehouse.default.training_documents_anonymized") ``` ### PII-kategorier relevant for norsk offentlig sektor | Kategori | Azure-kode | Eksempler | |----------|-----------|----------| | Personnummer | NorwayIdentityNumber | 01019012345 | | Personnavn | Person | Ola Nordmann | | Adresse | Address | Storgata 15, 0184 Oslo | | Telefonnummer | PhoneNumber | +47 90000000 | | E-post | Email | ola@firma.no | | Bankkonto | InternationalBankingAccountNumber | NO9386011117947 | | Organisasjonsnummer | Organization | 123 456 789 | | Helseinfo (PHI) | HealthcareEntities | Diagnose, medisin | --- ## Right-to-Be-Forgotten Implementation ### GDPR Artikkel 17: Retten til sletting ```python from delta.tables import DeltaTable class GDPRDeletionService: """ Implementer rett til sletting (GDPR Art. 17) i Delta Lake. """ def __init__(self, tables_with_personal_data: list): self.tables = tables_with_personal_data self.deletion_log_table = "lakehouse.default.gdpr_deletion_log" def process_deletion_request(self, person_id: str, request_id: str): """ Slett alle personopplysninger for en person pa tvers av tabeller. """ results = {} for table_config in self.tables: table_name = table_config["table"] id_column = table_config["id_column"] strategy = table_config.get("strategy", "hard_delete") try: delta_table = DeltaTable.forName(spark, table_name) if strategy == "hard_delete": # Slett raden helt delta_table.delete(f"{id_column} = '{person_id}'") elif strategy == "anonymize": # Anonymiser i stedet for a slette anon_columns = table_config.get("anonymize_columns", []) update_set = {col: F.lit("SLETTET") for col in anon_columns} update_set["is_anonymized"] = F.lit(True) update_set["anonymized_date"] = F.current_timestamp() delta_table.update( condition=f"{id_column} = '{person_id}'", set=update_set ) elif strategy == "pseudonymize": # Erstatt med pseudonym import hashlib pseudonym = hashlib.sha256( f"{person_id}_{request_id}".encode() ).hexdigest()[:12] delta_table.update( condition=f"{id_column} = '{person_id}'", set={id_column: F.lit(f"PSEUDO_{pseudonym}")} ) results[table_name] = {"status": "OK", "strategy": strategy} except Exception as e: results[table_name] = {"status": "ERROR", "error": str(e)} # Logg slettingen self._log_deletion(request_id, person_id, results) return results def _log_deletion(self, request_id, person_id, results): """Logg slettingsforesporselen for compliance-formaal.""" log_entry = spark.createDataFrame([{ "request_id": request_id, "person_id_hash": hashlib.sha256(person_id.encode()).hexdigest(), "timestamp": datetime.now().isoformat(), "tables_processed": len(results), "all_successful": all(r["status"] == "OK" for r in results.values()), "details": json.dumps(results) }]) log_entry.write.format("delta").mode("append") \ .saveAsTable(self.deletion_log_table) def vacuum_after_deletion(self, retention_hours: int = 0): """ Kjor VACUUM for a fysisk fjerne slettede data. ADVARSEL: Setter retensjon til 0 timer = ingen tidsreise mulig. """ spark.conf.set("spark.databricks.delta.retentionDurationCheck.enabled", "false") for table_config in self.tables: spark.sql(f"VACUUM {table_config['table']} RETAIN {retention_hours} HOURS") spark.conf.set("spark.databricks.delta.retentionDurationCheck.enabled", "true") # Konfigurasjon tables_config = [ {"table": "lakehouse.default.customers", "id_column": "person_id", "strategy": "hard_delete"}, {"table": "lakehouse.default.transactions", "id_column": "customer_id", "strategy": "anonymize", "anonymize_columns": ["customer_name", "email", "phone"]}, {"table": "lakehouse.default.ml_features", "id_column": "entity_id", "strategy": "pseudonymize"}, {"table": "lakehouse.default.embeddings", "id_column": "source_person_id", "strategy": "hard_delete"} ] gdpr_service = GDPRDeletionService(tables_config) result = gdpr_service.process_deletion_request("12345678901", "REQ-2026-001") ``` ### TTL (Time-to-Live) for automatisk sletting ```python # Implementer TTL for partisjonerte Delta-tabeller def enforce_ttl(table_name: str, partition_column: str, retention_days: int): """ Slett partisjoner eldre enn retention_days. Nyttig for a overholde GDPR-krav om minimering av lagringstid. """ cutoff_date = (datetime.now() - timedelta(days=retention_days)).strftime("%Y-%m-%d") delta_table = DeltaTable.forName(spark, table_name) delta_table.delete(f"{partition_column} < '{cutoff_date}'") # VACUUM for a fysisk fjerne filene spark.sql(f"VACUUM {table_name}") print(f"Slettet data eldre enn {cutoff_date} fra {table_name}") # Kjor daglig: Slett persondata eldre enn 13 maaneder enforce_ttl("lakehouse.default.customer_interactions", "interaction_date", retention_days=395) ``` --- ## Privacy Impact Assessments ### DPIA-rammeverk for AI-systemer | Fase | Aktivitet | Verktoy | |------|----------|---------| | **1. Kartlegging** | Identifiser persondata i AI-systemet | Microsoft Purview Data Map | | **2. Vurdering** | Vurder noodvendighet og proporsjonalitet | DPIA-mal fra Datatilsynet | | **3. Risikoanalyse** | Identifiser risiko for de registrerte | Risk Assessment Framework | | **4. Tiltak** | Implementer tekniske og organisatoriske tiltak | Anonymisering, tilgangsstyring | | **5. Dokumentasjon** | Dokumenter vurderingen | Protokoll, behandlingsregister | | **6. Konsultasjon** | Konsulter personvernombud / Datatilsynet | Ved hoy risiko | ### Automatisert personvern-sjekk i CI/CD ```python def privacy_check_before_deployment(model_artifacts_path: str) -> dict: """ Automatisert personvernsjekk for ML-modeller. Kjores som del av CI/CD-pipeline. """ checks = {} # 1. Sjekk at treningsdata er anonymisert training_data_path = f"{model_artifacts_path}/training_data_manifest.json" manifest = json.load(open(training_data_path)) checks["anonymized_training_data"] = manifest.get("anonymization_applied", False) # 2. Sjekk at modellen ikke memorerer PII # (Sample inference med kjente PII-verdier) checks["pii_leakage_test"] = run_pii_leakage_test(model_artifacts_path) # 3. Sjekk at DPIA er utfylt checks["dpia_completed"] = os.path.exists(f"{model_artifacts_path}/dpia_signed.pdf") # 4. Sjekk at personvernombud er konsultert checks["dpo_approved"] = manifest.get("dpo_approval_date") is not None # 5. Sjekk retensjonspolicy checks["retention_policy_defined"] = manifest.get("data_retention_days") is not None all_passed = all(checks.values()) return { "passed": all_passed, "checks": checks, "recommendation": "DEPLOY" if all_passed else "BLOKKER - Personvernkrav ikke oppfylt" } ``` --- ## Referanser - [What is Azure Language PII detection?](https://learn.microsoft.com/en-us/azure/ai-services/language-service/personally-identifiable-information/overview) -- PII-deteksjon og maskering - [PII filter in Azure AI Foundry](https://learn.microsoft.com/en-us/azure/ai-foundry/openai/concepts/content-filter-personal-information) -- PII-filtrering for LLM-er - [Responsible AI - Privacy and security](https://learn.microsoft.com/en-us/azure/machine-learning/concept-responsible-ai) -- SmartNoise og Counterfit - [Data privacy for cloud-scale analytics](https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/scenarios/cloud-scale-analytics/secure-data-privacy) -- Dataklassifisering og konfidensialitetsskjema - [PII entity categories](https://learn.microsoft.com/en-us/azure/ai-services/language-service/personally-identifiable-information/concepts/entity-categories) -- Alle stottede PII-kategorier - [Transparency note for PII](https://learn.microsoft.com/en-us/azure/ai-foundry/responsible-ai/language-service/transparency-note-personally-identifiable-information) -- Bruksomrader og begrensninger - [Data governance with Microsoft Purview](https://learn.microsoft.com/en-us/purview/data-governance-master-data-management) -- Purview for dataklassifisering --- ## For Cosmo - **Bruk denne referansen** naar kunder behandler personopplysninger i AI-systemer og trenger anonymiserings- og personvernstrategier. - **Azure Language PII-deteksjon er forstevalget** for a identifisere og maskere personopplysninger i tekst -- bade for treningsdata og RAG-dokumenter. Stotter norsk sprak. - **GDPR-sletting i Delta Lake krever VACUUM**: Delta Lake sin tidsreise betyr at slettede data forblir tilgjengelige i transaksjonsloggen til VACUUM kjores. Planlegg VACUUM i trad med slettekrav. - **K-anonymitet er minimum for publisering**: For datasett som deles utenfor organisasjonen, krev minimum k=5 anonymitet. For helsedata, bruk l-diversitet i tillegg. - **For norsk offentlig sektor**: Datatilsynets DPIA-mal er obligatorisk for AI-systemer med hoy risiko. Integrer personvernsjekk i CI/CD-pipeline for a sikre at ingen modell deployes uten godkjent DPIA.