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.
77 lines
4.4 KiB
Markdown
77 lines
4.4 KiB
Markdown
# 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)
|
|
|
|
- [x] Confirm the source is actually needed and no anonymised/official
|
|
alternative exists (official register > directory listing).
|
|
- [x] 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).
|
|
- [x] **VERIFY current terms + robots.txt + availability** on the live site.
|
|
Do not hardcode "catsit.com allows" — re-check each enable.
|
|
- [x] 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).
|
|
- [x] Define the minimum `field_allowlist` (e.g. only `street_address`, not
|
|
`phone`).
|
|
- [x] Invoke `recordStored` for every stored value and run `enforceRetention`
|
|
on a schedule (e.g. daily cron).
|
|
- [x] 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). |