Files
admin 17bd1b40a0 feat(0pt): record legitimate-interest stance — B2B contacts, company-linked only
Recorded decision (Simon 2026-08-31): personal data may be stored under
legitimate interest ONLY as business contacts bound to a company/org entity.
Gate enforces it: legitimate_interest requires purpose=business_contact and
entity_linked_only=true; recordStored() requires entity_id (orgnr/rgs/LEI,
hashed) and refuses person-only rows. +1 test; suite 167 pass | 1 skip;
typecheck clean.
2026-09-15 02:40:52 +02:00

4.4 KiB

PII Guardrail — GDPR requirement for personal-data sources (0pt)

Status: enforced, fail-closed. No 0pt pipeline is crawling personuppgifter today; the PiiGateService ships so that any future source that touches personal data (Ratsit, Merinfo, Hitta, …) is technically GDPR-OK before the first byte is stored.

The rule

  1. Classify. A source is personal-data if it self-flags (touches_personal_data) or is a known directory domain (ratsit.se, merinfo.se, hitta.se, mrkoll.se, eniro.se, birthday.se, upplysning.se).
  2. No policy → no processing. assertCanStore() throws unless a PiiPolicy with a valid legal_basis + basis_ref is on record.
  3. Everything is evidenced. Each decision is emitted to AUD0 at risk L3 (0pt.pii_policy_created | pii_data_retained | pii_erased | pii_retention_purged).

Recorded legitimate-interest stance (Simon, 2026-08-31)

"Vi får lagra dem om dem är kopplade till företagen i affärssyfte."

Personal data may be stored under legitimate interest (art. 6(1)(f)) ONLY as business contacts bound to a company/org entity (entity_linked_only). The gate enforces this mechanically:

  • legal_basis: legitimate_interest ⇒ purpose MUST be business_contact and entity_linked_only MUST be true. A lazily-written policy is rejected.
  • recordStored() refuses person-only rows for such policies: an entity_id (orgnr/rgs-nummer/LEI) is REQUIRED; only its hash is ever stored.
  • Stand-alone personal records (no backing company) are not permitted under LI.

This is the recorded business-policy decision — a written LIA should be kept and its id referenced in basis_ref (lia-YYYY-MM-b2b-NN naming is fine).

GDPR article mapping

Requirement Implementation
Legal basis — art. 6 legal_basis ∈ legitimate_interest/consent/contract/legal_obligation + basis_ref (LIA doc id, consent record id, contract clause, provision). Cannot be empty.
Data minimisation — art. 5(1)(c) field_allowlist; storing any other field throws PiiMinimisationError. Empty allowlist = nothing stored.
Storage limitation — art. 5(1)(e) retention_days; enforceRetention() purges expired rows. 0 = fetch-and-drop, nothing persisted.
Integrity/confidentiality — art. 32 Only sha256 hashes of identity keys stored (recordStored); plaintext identity never written; log/trace redaction (redact()) for personnummer/e-mail/phone.
Right of access — art. 15 accessStatement(subjectHash) — what is held on a subject, across sources.
Right to erasure — art. 17 erase({ subject_hash, reason }) tombstones every stored value for the subject.
Data protection by design — art. 25 Gate defaults ON in SourceQueryService; no bypass knobs.

When someone requests a personal-data source (e.g. Ratsit)

  • Confirm the source is actually needed and no anonymised/official alternative exists (official register > directory listing).
  • Write the Legitimate Interest Assessment (or record consent / contract / legal obligation) and store its id in basis_ref. LIA minimum: purpose, necessity, individuals' rights impact, mitigations. For LI: purpose business_contact + entity_linked_only (see recorded stance above — an LIA referencing lia-YYYY-MM-b2b-NN is good practice).
  • VERIFY current terms + robots.txt + availability on the live site. Do not hardcode "catsit.com allows" — re-check each enable.
  • Set robots_and_rate_confirmed: true + a SourceRatePolicy matching the source's own limits (legacy N0D default for ratsit.se was 1/min — treat that as a ceiling, not a target).
  • Define the minimum field_allowlist (e.g. only street_address, not phone).
  • Invoke recordStored for every stored value and run enforceRetention on a schedule (e.g. daily cron).
  • Record a DPIA note if processing is "large scale" (art. 35) — bulk directory scraping of living persons is large-scale by default.

Never

  • Pretend a source has a legal basis it does not have; basis_ref must point to a real artifact.
  • Store identity keys in plaintext (pseudonymize_keys defaults to true).
  • Use stealth/bot-bypass/rate-evasion — excluded by CLAUDE.md §1 regardless of GDPR.
  • Claim compliance on a source we have not re-verified this season (VERIFY markers are intentional).