dapla-toolbelt-pseudo

Sist endret

March 18, 2025

Dapla logo

dapla-toolbelt-pseudo er en python-pakke som har som sitt hovedformål å gi Dapla-brukere muligheten til å pseudonymisere, de-pseudonymisere og re-pseudonymisere data. Det skal sikre at brukerne av Dapla har verktøyene de trenger for å jobbe med direkte identifiserende opplysninger i henhold til lovverk og SSBs tolkninger av disse.

Siden tilgang til direkte identifiserende opplysninger er underlagt strenge regler, så krever bruken av dapla-pseudo-toolbelt at man forholder seg til vedtatte standarder som datatilstander og systemer som Kildomaten. I tillegg er det en streng tilgangsstyring til hvor man kan kalle funksjonaliteten fra. Tjenestene er satt opp på en slik måte at Dapla-team skal være selvbetjent i bruken av funksjonaliteten, samtidig som regler, prosesser og standarder etterleves på enklest mulig måte.

ImportantStandardisert klassifisering av datatilstander

I SSB er det bestemt at all data skal klassifiseres på en standardisert måte basert på datatilstander for å avgjøre om de er sensitive, skjermet eller åpen. Den eneste datatilstanden som klassifiseres som sensitiv er kildedata. Det er derfor bestemt at pseudonymisering er en av prosesseringene som skal skje mellom datatilstandene kildedata og inndata.

Forberedelser

Før man tar i bruk funksjonaliteten er det viktig at man kjenner godt til tilgangstyring i Dapla-team og Kildomaten, og har diskutert med seksjonen hvordan man skal behandle direkte identifiserende opplysninger i de aktuelle dataene.

For at et Dapla-team skal kunne bruke dapla-toolbelt-pseudo må Kildomaten være skrudd på for miljøet1 man ønsker å jobbe fra. Som standard får alle statistikkteam skrudd på Kildomaten i prod-miljøet og ikke i test-miljøet. Ønsker du å aktivere Kildomaten i test-miljøet kan dette gjøres selvbetjent som en feature.

Tilgangsstyring

Tilgang til å funksjonalitet i dapla-toolbelt-pseudo kan regnes som sensitivt i seg selv, og derfor er det en streng tilgangsstyring for bruk av tjenesten. I prod-miljøet kan man kun ta i bruk funksjonaliteten ved å prosessere dataene i Kildomaten, og det er bare tilgangsgruppen data-admins som har tilgang til å godkjenne slike automatiske prosesseringer. I test-miljøet derimot kan alle på teamet benytte seg av all funksjonalitet, siden det aldri skal forekomme ekte data her.

Tabell 1: Tilgangsstyring til dapla-pseudo-toolbelt
(a) Test-miljø
Aktør Validator Pseudonymize() Depseudonymize() Repseudonymize()
Kildomaten
data-admins (interaktivt)
developers (interaktivt)
(b) Prod-miljø
Aktør Validator Pseudonymize() Depseudonymize() Repseudonymize()
Kildomaten 🚫 🚫
data-admins (interaktivt) 🚫 🚫 🚫 🚫
developers (interaktivt) 🚫 🚫 🚫 🚫

I Tabell 1 ser vi fra Tabell 1 (a) at man i test-miljøet har full tilgang til funksjonaliteten i dapla-toolbelt-pseudo, både fra Kildomaten og når man jobber interaktivt2 i Jupyterlab. Tabell 1 (b) viser at det kun er tilgang til pseudonymize() og validator() fra Kildomaten i prod-miljøet, og man kan aldri interaktivt kan kalle på funksjoner som potensielt avslører et pseudonym. Av den grunn er det alltid anbefalt å teste ut koden sin i test-miljøet før den produksjonssettes i i prod-miljøet med Kildomaten.

CautionUlike pseudonymer i prod og test

Selv om man har videre rettigheter til å bruke funksjonaliteten i dapla-toolbelt-pseudo fra test-miljøet sammenlignet med prod-miljøet, så betyr ikke det at samme input i de to miljøene vil samme output. Når funksjonaliteten kalles fra test-miljøet så benyttes det automatisk en annen krypteringsnøkkel enn den som benyttes i prod. På den måten vil et pseudonym produsert fra prod-miljøet aldri være likt det som produseres fra prod-miljøet selv om input skulle være den samme.

Funksjonalitet

I denne delen viser vi hvilken funksjonalitet som tilbys gjennom dapla-toolbelt-pseudo. Eksempelkoden under viser hvordan man ville kjørt det fra en notebook i test-miljøet til teamet, og ikke hvordan koden må skrives når det skal kjøres i Kildomaten3.

Installering

dapla-toolbelt-pseudo er ferdig installert i Kildomaten. Men ønsker du å bruke den i test-miljøet til teamet så kan du installere det i et ssb-project fra PyPI med denne kommandoen:

Terminal
poetry add dapla-toolbelt-pseudo

Dataformater

dapla-toolbelt-pseudo støtter innlesing av følgende dataformater:

Eksemplene under viser hovedskelig innlesing av dataframes fra minnet4, men du kan lese mer om filbasert prosessering lenger ned i kapitlet.

Pseudonymisering

Pseudonymisering tilbys via Pseudonymize-metoden i dapla-toolbelt-pseudo. Den følger et builder-pattern der man spesifiserer hva og i hvilken rekkefølge operasjonene skal gjøres. Anta at det finnes en Polars dataframe i minnet som heter df hvor kolonnen fnr skal pseudonymiseres. Da vil koden se slik ut:

Notebook
from dapla_pseudo import Pseudonymize

result_df = (
    Pseudonymize.from_polars(df) 
    .on_fields("fnr")
    .with_default_encryption()                     
    .run()                                
    .to_polars()                                   
)

I koden over så angir from_polars(df) at kolonnen vi ønsker å pseudonymisere ligger i en Polars dataframe i minnet og heter df. Deretter spesifiserer vi at kolonnen fnr er den som skal behandles med funksjonen on_fields("fnr"). Videre angir with_default_encryption() at fnr skal pseudonymiseres med dapla-toolbelt-speudo sin standard-algoritme5. Til slutt ber vi om at det ovennevnte blir kjørt med funksjonen run(), og at dataene skal returneres som en Polars dataframe med funksjonen to_polars().

Se flere eksempler i dokumentasjonen.

De-pseudonymisering

De-pseudonymisering tilbys via Depseudonymize-metoden i dapla-toolbelt-pseudo. Den følger et builder-pattern der man spesifiserer hva og i hvilken rekkefølge operasjonene skal gjøres. Anta at det finnes i en Polars dataframe i minnet som heter df hvor kolonnen fnr skal de-pseudonymiseres. Da vil koden se slik ut:

Notebook
result_df = (
    Depseudonymize.from_polars(df)            
    .on_fields("fnr")                          
    .with_default_encryption()                     
    .run()                                         
    .to_polars()                                   
)

Oppbygningen av koden med Depseudonymize() er helt lik som for Pseudonomize(). Les beskrivelsen der for å se hva de ulike funksjonskallene gjør. Se flere eksempler i dokumentasjonen.

De-pseudonymisering er også støttet for informasjon som først er transformert til stabil ID og deretter pseudonymisert med Papis-nøkkelen. I disse tilfellene kan det også være behov for å spesifisere hvilken versjon av snr-katalogen man ønsker å benytte for å erstatte snr med fødselsnummer:

Notebook
from dapla_pseudo import Depseudonymize

result_df = (
    Depseudonymize.from_pandas(df)            
    .on_fields("fnr")                          
    .with_stable_id(
      sid_snapshot_date="2023-05-29")                    
    .run()                                         
    .to_pandas()                                   
)

I eksempelet over spesifiserer vi datoen 2023-05-29 og da benyttes snr-katalogen som ligger nærmest i tid til denne datoen. Hvis sid_snapshot_date ikke oppgis benyttes siste tilgjengelige versjon av katalogen.

ImportantDe-pseudonymisering ikke tilgjengelig i prod-miljø

Foreløpig er det kun tilgang til å pseudonymisere i test-miljøet med test-data. Ta kontakt med Dapla-teamene dersom det dukker opp behov for å kunne de-pseudonymisere i prod-miljøet.

Re-pseudonymisering

Under utvikling.

Stabil ID

I statistikkproduksjon og forskning er det viktig å kunne følge de samme personene over tid. Derfor har fødselsnummer ofte blitt oversatt til en mer stabilt identifikator, ofte kalt SNR eller stabil ID6. Funksjonene with_stable_id() og validator() bruker stabilID-katalogen til å henholdsvis bytte ut fødselsnummer med stabil ID, og for å validere om fødselsnummer er gyldige(se under).

Du kan selv spesifisere hvilken versjon av SNR-katalogen du ønsker å bruke. Det gjør du ved å oppgi en gyldighetsdato og så finner dapla-toolbelt-pseudo hvilken versjon av katalogen som ligger nærmest i tid.

Papis-pseudonym

Dapla tilbyr samme pseudonym som Papis-prosjektet7. Denne kan benyttes på 2 måter:

  1. Pseudonymisere hvilken som helst informasjon med samme nøkkel som Papis.
  2. Transformere fødselsnummer til snr-nummer og deretter pseudonymisere med samme nøkkel som Papis.

Punkt 1 er nyttig for de som har pseudonymisert informasjon på bakken tidligere og vil ha samme pseudonym på Dapla8. Dette kan gjelde hvilken som helst informasjon, også direkte pseudonymisering av fødselsnummer, uten at det er gått via snr-nummer. Her er et eksempel på hvordan man pseudonymiserer på denne måten:

Notebook
result = (
    Pseudonymize.from_pandas(df)
    .on_fields("fornavn")                      
    .with_papis_compatible_encryption()         
    .run()                               
    .to_pandas()                                  
)

Punkt 2 over er nok den som benyttes mest i SSB siden den sikrer at pseudonymisert fødselsnummer kan kobles med data som er pseudonymisert på bakken. Her er et eksempel på hvordan man pseudonymiserer snr med Papis-nøkkelen:

Notebook
result = (
    Pseudonymize.from_pandas(df)
    .on_fields("fnr")                      
    .with_stable_id()         
    .run()                               
    .to_pandas()                                  
)
NoteHva betyr det å tilby samme pseudonym?

At Papis og Dapla tilbyr samme pseudonum betyr egentlig at vi bruker samme krypteringsalgoritme, og en felles krypteringsnøkkel. Krypteringsalgoritmen som benyttes er formatpreserverende (FPE) og biblioteket som brukes er Tink FPE Python. En begrensning med algorimen er at kun karakterer som finnes i et forhåndsdefinert karaktersett (tall, store og små bokstaver fra a-z) blir vurdert. Andre karakterer f. eks æøå blir ikke kryptert. Papis-nøkkelen (som brukes f. eks for fnr og snr) benytter en SKIP-strategi for karakterer som faller utenom, som betyr at algoritmen simpelthen “hopper over” disse. FPE-algoritmen er også avhengig av størrelsen på det predefinterte karaktersettet for å avgjøre minimumslengden på teksten som pseudonymiseres. For Papis-nøkkelen betyr det at teksten minst må være 4 karakterer lang. Kortere tekster blir ikke kryptert.

Validere fødselsnummer

Validator-metoden kan benyttes til å sjekke om fødselsnummer finnes i SNR-katalogen (se over), og returnerer de ugyldige fødselsnummerne tilbake. Her kan man også spesifisere hvilken versjon av SNR-katalogen man ønsker å bruke. Standard, hvis ingenting velges, er at siste tilgjengelige versjon velges. Under er et eksempel på hvordan man validerer fødselsnummer for en gitt gyldighetsdato:

Notebook
from dapla_pseudo import Validator

result = (
    Validator.from_polars(df)
    .on_field("fnr")
    .validate_map_to_stable_id(
        sid_snapshot_date="2023-08-29"
    )
)
# Vis hvilken versjon av SNR-katalogen som er benyttet
result.metadata
# Vis fødselsnummer som ikke fikk treff i SNR-katalogen som en Polars dataframe
result.to_polars()
# Eventuelt som en Pandas DataFrame
result.to_pandas()

Se flere eksempler i dokumentasjonen.

Wildcards

Kommer snart.

Dataminimering

Kommer snart.

Algoritmer

Følgende sammentsetninger av algoritmer, nøkler og SID-mapping støttes på Dapla:

Tabell 2: Oversikt over algoritmer, nøkler og mapping som støttes på Dapla.
Algoritme dapla-toolbelt-pseudo Nøkkelnavn Støtter SID-mapping
TINK-DAED with_default_encryption ssb-common-key-1 Nei
TINK-DAED with_default_encryption ssb-common-key-2 Nei
TINK-FPE with_stable_id papis-common-key-1 Ja
TINK-FPE with_papis_compatible_encryption papis-common-key-1 Nei

Metadata

Det genereres to typer av metadata når man pseudonymiserer:

  1. Datadoc
  2. Metadata

De to typene av metadata returneres til brukeren i to forskjellige objekter.

Datadoc

Datadoc-metadata er på et format som er integrert i Datadoc9. I koden til høyre så printes metadataene fra et kall til Pseudonymize ved å skrive print(result.datadoc). Da printer man objektet interaktivt i f.eks. Jupyter. Merk at dette kun er mulig i test-miljøet

Det er anbefalt å ta vare på Datadoc-metadataen i Kildomaten. Da er det lettere å skrive filen direkte i bøtta med to_file()-funksjonen, også i kodesnutten til høyre. to_file("<filsti>.parquet") skriver to filer til filstien - en fil med de pseudonymiserte dataene, og en Datadoc-fil med samme filnavn, men da med endelsen __DOC på slutten av filnavnet. Med denne funksjonen slipper man å tenke på om filen skrives med riktig formattering.

Notebook
result = (
    Pseudonymize.from_polars(df)    
    .on_fields("fnr")           
    .with_stable_id()
    .run()                      
)

1print(result.datadoc)
2result.to_file("gs://bucket/test.parquet")
1
Printer metadata i en Notebook.
2
Skriver data og metadata direkte til bøtte.

Når man kjører pseudonymisering fra Kildomaten er det viktig å tenke på at felter som er pseudonymisert ikke må endres uten at metadataene også endrer. Da kan man risikere at metadatene ikke lenger beskriver riktig informasjon.

Under ser man hvilken informasjon som genereres fra pseudonymiseringen.

Datadoc
[
  {
    "short_name": "fnr",
    "data_element_path": "fnr",
    "pseudonymization": {
      "stable_identifier_type": "FREG_SNR",
      "stable_identifier_version": "2023-08-31",
      "encryption_algorithm": "TINK-FPE",
      "encryption_key_reference": "papis-common-key-1",
      "encryption_algorithm_parameters": [
        {
          "keyId": "papis-common-key-1"
        },
        {
          "snapshotDate": "2023-08-31"
        },
        {
          "strategy": "skip"
        }
      ]
    }
  }
]

Datadoc-metadataen er en liste av pseudonymiserte variabler. Av metadatene kan vi i dette tilfellet se at det kun er ett element i listen - med andre ord at det bare var feltet fnr som ble pseudonymisert. Vi ser også av stable_identifier_type at fnr ble oversatt til snr, og at versjonen av SNR-katalogen var fra 2023-08-31. encryption_algorithm angir at det var det var den formatpreserverende algoritmen TINK-FPE som ble benyttet. keyID: "papis-common-key-1" angir hvilken nøkkel-id som ble benyttet. strategy: "SKIP" refererer til at den format-preserverende algoritmen skal “hoppe over” ugyldige karakterer og la de være som de er.

Denne informasjonen vil være svært nyttig i SSB hvis man senere skal kunne de-pseudonymisere eller re-pseudonymisere data.

Legge til pseudonymiseringsmetadata for allerede eksisterende Datadoc-metadata

Hvis man allerede har Datadoc-metadata for et datasett, og man ønsker å legge til pseudonymiseringsmetadata, kan man gjøre dette ved å bruke with_metadata()-metoden.

with_metadata(datadoc_metadata) tar et Datadoc-objekt som et argument. Datadoc-objektet for et datasett fås ved hjelp av dapla-toolbelt-metadata. Les mer om pakken her.

Datadoc-metadata
result = (
    Pseudonymize.from_pandas(df)
    .with_metadata(datadoc_obj)
    .on_fields("fnr")                      
    .with_stable_id()         
    .run()                              
)
result.to_file("gs://somebucket/somefile.parquet") # NB: Husk å skrive tilbake data og metadata!

Metadata

Den andre typen av metadata kan hentes ut etter et kall til Pseudonymize med kommandoen result.metadata. Den returnerer en python dictionary. Den inneholder hovedsaklig logginformasjon og metrikker foreløpig. For de som pseudonymiserer med with_stable_id() kan output se slik ut:

Metadata
{
    'fnr': {
        'logs': [
            'No SID-mapping found for fnr 999999*****',
            'No SID-mapping found for fnr XX',
            'No SID-mapping found for fnr X8b7k2*'
        ],
        'metrics': [
            {'MAPPED_SID': 10},
            {'MISSING_SID': 3}
        ]
    }
}

I Kildomaten kan det vært nyttig å printe denne informasjonen til loggene. Av eksempelet over ser vi at verdier som er over 6 karakterer lange blir maskert.

Prosessering av store datasett

Når man pseudonymiserer med Pandas eller Polars DataFrames må hele datasettet leses inn i minnet for at det skal kunne pseudonymiseres. Det kan være problematisk hvis datasettet er stort, fortrinnsvis i Kildomaten. For å løse dette problemet tilbyr dapla-toolbelt-pseudo muligheten til å strømme alle kolonner i datasettet som ikke skal pseudonymiseres - upseudonymiserte kolonner blir da delt opp i mindre biter (“chunks”) når den nye filen skrives til bøttene. Man trenger da kun å holde de pseudonymiserte kolonnene i minnet, noe som kan betydelig redusere minnebruken.

Eksempel for Kildomaten:

Strømming i Kildomaten
import polars as pl
from dapla_pseudo import Pseudonymize

def main(source_file):
    # Les inn data som en "lazy" DataFrame
    lazy_df = pl.scan_csv(source_file)

    result = Pseudonymize.from_polars(lazy_df).on_fields("fnr").with_stable_id().run()

    output_file = source_file.replace("kilde", "produkt")
    result.to_file(output_file)

Funksjonaliteten lener seg på Polars’ LazyFrames. I stedet for pl.read_csv() som leser hele datasettet inn i minnet, bruker man pl.scan_csv() - denne funksjonen leser inn dataene som en “lazy” DataFrame, kalt en LazyFrame.

Når man skriver eller henter ut resultatet av pseudonymiseringen, må man bruke to_file()-funksjonen for å strømme dataen til fil som i eksempelet over, eller result.to_polars_lazy() for å få resultatet som en LazyFrame. Merk at hvis man har flere mellomsteg i koden mellom innlesning og pseudonymisering, kan man måtte tilpasse koden for LazyFrames. Man kan lese mer i Polars-dokumentasjonen her.

Important

Strømming av data støttes ikke med hierarkiske filer.

Pseudonymisering av hierarkisk data

Hierarkisk data er data som har en trestruktur, og som ofte lagres i formater som JSON eller XML. Hierarkisk data ser slik ut:

hierarchical_data.json
[
    { // Person 1
    "person_info": {
        "fnr": "11854898347",
        "fornavn": "Donald",
        "etternavn": "Duck"
    },
    "kjonn": "M",
    "fodselsdato": "020995"
    },

    { // Person 2
    "person_info": {
        "fnr": "01839899544",
        "fornavn": "Mikke",
        "etternavn": "Mus"
    },
    "kjonn": "M",
    "fodselsdato": "060970"
    },
]

I dette eksempelet er det det nøstede feltet “person_info” som gjør strukturen hierarkisk. Dette står i kontrast til “flate” data, hvor man gjerne kun har en tabellstruktur som i en CSV-fil.

Pseudonymisering av hierarkisk data er noe mer komplisert enn pseudonymisering av flate data, da man må beskrive hvor i den hierarkiske strukturen man skal pseudonymisere. Dette gjøres ved å lage såkalte PseudoRules.

En PseudoRule har hovedsakelig tre komponenter:

  1. Function: Beskriver enkrypteringsalgoritmen som skal brukes for feltene i path. Eksempel: map-sid-ff31(sid-snapshot-date=2019-12-31)
  2. Path: En beskrivelse av nøyaktig hvor i den hierarkiske strukturen det skal pseudonymiseres. I eksempelet over vil pathen til fnr være person_info/fnr, og pathen til kjonn vil enkelt nok være kjonn. Skråstreken bestemmer hvilket nivå i hierarkiet man peker på.
  3. Pattern: Glob-uttrykk som beskriver hvor i den hierarkiske strukturen man skal pseudonymisere. Eksempel: **/fnr

Hvordan man genererer de tre feltene vil bli beskrevet senere.

Pseudonymisering av hierarkisk data

Typisk vil man ha flere PseudoRules for å kunne pseudonymisere flere felt i den hierarkiske strukturen. Da er det vanlig å samle alle PseudoRules in en felles .json-fil, som kan se noe slik ut for eksempelet over:

pseudo_rules.json
[
    {
        "func": "map-sid-ff31(sid-snapshot-date=2019-12-31)",
        "path": "person_info/fnr",
        "pattern": "**/fnr"
    },
    {
        "func": "daead()",
        "path": "person_info/fornavn",
        "pattern": "**/fornavn"
    },
    {
        "func": "daead()",
        "path": "person_info/etternavn",
        "pattern": "**/etternavn"
    }
]

Ved å lese denne JSON-filen sammen med dataene, forstår dapla-toolbelt-pseudo nøyaktig hvilke felter som skal pseudonymiseres, hvor de befinner seg i den hierarkiske strukturen, og hvilken algoritme som skal benyttes for å pseudonymisere hvert felt. Man kan da bruke dapla-toolbelt-pseudo slik for å pseudonymisere:

process_source_data.py
import json
import pandas as pd
from dapla_pseudo import Pseudonymize, PseudoRule

RULES_FILENAME = "gs://<kildebøtte-navn>/pseudo_rules.json"

def main(source_file: str):
    with open(RULES_FILENAME, 'r') as rules_file:
        pseudo_rule_list = json.load(file)
    
    # Konverter fra Python dictionaries til PseudoRule-objekter
    pseudo_rules = [PseudoRule.from_json(rule) for rule in pseudo_rule_list]

    df = pd.read_json(source_file) # Eksempel på innlesning av data, tilpass etter behov

    result = (
        Pseudonymize
        .from_pandas(df) 
        .add_rules(pseudo_rules)
        .run(hierarchical=True) # Viktig å spesifisere at dataene er hierarkiske!
    )

    output_file = source_file.replace("kilde", "produkt").replace(".json", ".parquet")

    result.to_file(output_file) # Eksempel på skriving av data, tilpass etter behov

PseudoRules - anatomi og oppsett

Som sagt er en PseudoRule en sammensetning av tre komponenter: func, path og pattern. Vi kommer til å se på hvordan man forholder seg til disse feltene når man setter opp PseudoRules for datagrunnlaget sitt

func

func beskriver hvilken algoritme som skal benyttes for å pseudonymisere det aktuelle feltet. I JSON-strukturen over supplerer man en enkel streng som korresponderer til algoritmen. Dette er veldig likt måten man bruker dapla-toolbelt-pseudo ellers - det er bare en annen syntaks. Bruk tabellen under:

Algoritme dapla-toolbelt-pseudo (flat) dapla-toolbelt-pseudo (hierarkisk) SID-mapping
TINK-DAED with_default_encryption() daead() Nei
TINK-FPE with_stable_id() map-sid-ff31() Ja
TINK-FPE with_papis_compatible_encryption() ff31() Nei
Redact10 Ikke tilgjengelig redact() Nei

I tillegg til funksjonene i tabellen over, kan man også sende inn argumenter til funksjonene i strengen. Disse argumentene må være på formatet func(arg=value). For eksempel, for å spesifisere hvilken versjon av SNR-katalogen, bruker man map-sid-ff31(sid-snapshot-date=2019-12-31). Merk at argumentene bruker bindestrek -, i motsetning til funksjonene i flat format som bruker understrek _.

path

En beskrivelse av nøyaktig hvor i den hierarkiske strukturen det skal pseudonymiseres. I eksempelet over vil pathen til fnr være person_info/fnr, og pathen til kjonn vil enkelt nok være kjonn.

pattern

Pattern er et glob-uttrykk som kan brukes til å generere paths for flere felt i den hierarkiske strukturen - fremgangsmåten for dette blir beskrevet under. Glob-uttryk er et mønster som forsøker å finne “matchende” strenger, det vil si strenger som følger mønsteret i glob-uttrykket.

Det viktigste symbolet er *, også kjent som wildcard-symbolet. * kan erstatte hvilket som helst felt i en path på et gitt nivå i hierarkiet. Dobbel-stjernesymbolet ** kan erstatte hvilket som helst felt i en path uavhengig av hierarkinivå. Noen eksempler:

  • */fnr vil matche alle strenger på det første nivået i hierarkiet, og søker etter fnr på det andre nivået. Dette patternet vil for eksempel matche person_info/fnr og person_data/fnr, og anything/fnr, men ikke person_info/barn/fnr fordi FNR-et her ligger på det tredje nivået i hierarkiet.
  • person_*/fnr vil matche både person_info/fnr og person_data/fnr, men ikke something/fnr
  • **/fnr vil matche på alle paths som slutter på fnr, uavhengig av hvor i hierarkiet det befinner seg. Det vil for eksempel matche person_info/fnr, person_info/barn/fnr og fnr

Hvordan generere PseudoRules

Enkel struktur

For hierarkiske data med en enkel struktur, vil det lønne seg å manuelt skrive PseudoRules i en JSON-fil som i eksempelet over. Disse lagres i kildebøtta, og kan leses inn i Kildomaten. Går man denne ruta, fyller man ut path for feltene man ønsker å pseudonymisere, velger func for hvert felt, og lar pattern = ** (match alle felter).

Kompleks struktur

For mer komplekse datasett med mange felt og/eller dypt hierarki, kan det være tidkrevende å manuelt skrive PseudoRules og spesielt da path for alle relevante felt. I disse tilfellene kan man generere PseudoRules ved å bruke pattern-feltet.

For å generere PseudoRules, trenger man:

  1. Et sett med delvis utfylte PseudoRules, hvor func og pattern er fylt ut. path skal ikke fylles ut - det er det som skal genereres. For hver PseudoRule, lag et pattern som matcher feltene man ønsker å pseudonymisere i datasettet. For dette vil man typisk bruke wildcard-karakterene * og **, som beskrevet over over.

  2. Dataenes schema. Et schema beskriver strukturen til dataene og alle mulige felter. Eksempel:

Data:

hierarchical_data.json
[
    {
    "person_info": {
        "fnr": "11854898347",
        "fornavn": "Donald",
        "etternavn": "Duck"
    },
    "kjonn": "M",
    "fodselsdato": "020995"
    },
]

Schema:

"person_info": Struct(
        "fnr": String,
        "fornavn": String,
        "etternavn": String
),
"kjonn": String,
"fodselsdato": String

Schemaene må være Parquet-schemaer, og blir typisk hentet fra API-definisjonen hos dataleverandør. Ta kontakt med Team Statistikktjenester for hjelp til å hente ut schema for dine data.

Med utgangspunkt i de delvis utfylte PseudoRules og schema, kan man utlede path for alle relevante felt i datasettet ved hjelp av SchemaTraverser()-klassen i dapla-toolbelt-pseudo. Dette kan gjøres i Dapla Lab, i et Poetry prosjekt med dapla-toolbelt-pseudo installert slik:

partial_pseudo_rules.json
[ // Utfylt i Dapla Lab, og lagres i kildebøtta
    {
        "func": "map-sid-ff31(sid-snapshot-date=2019-12-31)",
        "pattern": "**/fnr"
    },
    {
        "func": "daead()",
        "pattern": "**/fornavn"
    },
    {
        "func": "daead()",
        "pattern": "**/etternavn"
    }
]
generate_pseudo_rules.py
import json
import gcsfs
import polars as pl
from dapla_pseudo import PseudoRule
from dapla_pseudo import SchemaTraverser
from pydantic_core import to_jsonable_python

schema_path = "/buckets/kilde/someschema.parquet"

# Leser schema:
schema = pl.read_parquet_schema(
    schema_path
)

partial_pseudo_rules = json.load("partial_pseudo_rules.json")

rules = [PseudoRule.from_json(rule) for rule in partial_pseudo_rules]

sc = SchemaTraverser(schema, rules)

# Matcher reglene for pseudonymisering med feltene i schema:
pseudo_rules = sc.match_rules()

matched_rules_output_path = "buckets/kilde/pseudo_rules.json" # Brukes for pseudonymisering i Kildomaten
# Lagrer fil med matchede regler for pseudonymisering
def write_rules(file_path: str, rules: list[PseudoRule]) -> None:
    with gcsfs.GCSFileSystem().open(file_path, "w") as matched_rules_output:
        matched_rules_output.write(
            to_jsonable_python(rules)
        )

write_rules(matched_rules_output_path, concrete_rules)

Man kan da, med den ferdig utfylte pseudo_rules.json-filen, pseudonymisere dataene i Kildomaten ved å lese inn denne filen og sende den inn i add_rules()-funksjonen i dapla-toolbelt-pseudo som vist i eksempelet over.

Brukerveiledning

På grunn av den strenge tilgangsstyringen til dapla-pseudo-toolbelt og kildedata er det anbefalt å utvikle kode for overgangen fra kildedata til inndata i test-miljøet til teamet. I denne delen viser vi hvordan denne arbeidsflyten kan se ut, fra testing til en automatisert produksjon med ekte data, med et helt konkret eksempel.

Interaktiv utvikling

For å kunne kjøre pseudonymiseringen interaktivt i f.eks. en notebook i Jupyter, så må man jobbe i test-miljøet til teamet.

Kildomaten i test

Kommer snart.

Kildomaten i prod

Kommer snart.

Fotnoter

  1. Et Dapla-team har både et test- og et prod-miljø. Kildomaten må være skrudd på i det miljøet du ønkser å benytte dapla-toolbelt-pseudo fra.↩︎

  2. Med interaktiv jobbing menes at man skriver og kode og får tilbake output i samme verktøy. F.eks. er Jupyterlab et eksempel på et verktøy som lar deg jobbe interaktivt med data.↩︎

  3. I Kildomaten må koden blant annet pakke inn i main()-funksjon.↩︎

  4. Pandas og Polars dataframes er eksempler på dataformater som lever i minnet, og må konverteres før de skrives til et lagringsommråde. I praksis vil det ofte si at man jobber med dataframes når man jobber i verktøy som Jupyterlab, mens man skriver til lagringsområde når man er ferdig i Jupyterlab.↩︎

  5. Standardalgoritmen i dapla-toolbelt-pseudo er den deterministiske krypteringsalgoritmen Deterministic Authenticated Encryption with Associated Data, eller DAEAD-algoritmen.↩︎

  6. SNR-katalogen eies og tilbys av Team Register på Dapla.↩︎

  7. Papis var et prosjekt med fokus på bakkesystemene i SSB som skulle bringe SSBs behandling av personopplysninger, som brukes i statistikkproduksjon, i samsvar med GDPR gjennom en enhetlig pseudonymiseringsløsning.↩︎

  8. Generelt sett er det ikke å anbefale å benytte denne nøkkelen på annen informasjon enn fødselsnummer. Grunnen er at den er svakere enn andre algoritmer, der blant annet tekst som er kortere enn 4 karakter lang ikke blir pseudonymisert.↩︎

  9. Datadoc er det nye systemet for dokumentasjon av datasett i SSB.↩︎

  10. Redact er en algoritme som erstatter innholdet i et felt med karakteren *. Dette er ikke en enkrypteringsalgoritme, og det er ikke mulig å de-pseudonymisere data som har brukt redact. Det kan likevel være nyttig i noen tilfeller hvis man ønsker å fjerne irrelevant data.↩︎