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.
4.4 KiB
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
- 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). - No policy → no processing.
assertCanStore()throws unless aPiiPolicywith a validlegal_basis+basis_refis on record. - 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⇒purposeMUST bebusiness_contactandentity_linked_onlyMUST betrue. A lazily-written policy is rejected.recordStored()refuses person-only rows for such policies: anentity_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: purposebusiness_contact+entity_linked_only(see recorded stance above — an LIA referencinglia-YYYY-MM-b2b-NNis 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+ aSourceRatePolicymatching the source's own limits (legacy N0D default forratsit.sewas1/min— treat that as a ceiling, not a target). - Define the minimum
field_allowlist(e.g. onlystreet_address, notphone). - Invoke
recordStoredfor every stored value and runenforceRetentionon 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_refmust point to a real artifact. - Store identity keys in plaintext (
pseudonymize_keysdefaults 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 (
VERIFYmarkers are intentional).