ktg-plugin-marketplace/plugins/ms-ai-architect/skills/ms-ai-engineering/references/data-engineering/data-anonymization-privacy.md
Kjell Tore Guttormsen 6a7632146e feat(ms-ai-architect): add plugin to open marketplace (v1.5.0 baseline)
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>
2026-04-07 17:17:17 +02:00

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 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

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


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.