Initial addition of ms-ai-architect plugin to the open-source marketplace. Private content excluded: orchestrator/ (Linear tooling), docs/utredning/ (client investigation), generated test reports and PDF export script. skill-gen tooling moved from orchestrator/ to scripts/skill-gen/. Security scan: WARNING (risk 20/100) — no secrets, no injection found. False positive fixed: added gitleaks:allow to Python variable reference in output-validation-grounding-verification.md line 109. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
20 KiB
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
# 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
# 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:
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
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:
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:
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
# 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 | 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
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
# 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
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? -- PII-deteksjon og maskering
- PII filter in Azure AI Foundry -- PII-filtrering for LLM-er
- Responsible AI - Privacy and security -- SmartNoise og Counterfit
- Data privacy for cloud-scale analytics -- Dataklassifisering og konfidensialitetsskjema
- PII entity categories -- Alle stottede PII-kategorier
- Transparency note for PII -- Bruksomrader og begrensninger
- Data governance with Microsoft Purview -- 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.