getreadystack/personal-data-map-dsar-audit
0
1<!doctype html><html lang="en"><meta charset="utf-8">2<meta name="viewport" content="width=device-width,initial-scale=1">3<title>Personal Data Map: find every column a GDPR subject access request has to reach</title>4<meta name="description" content="Reads a migration, Prisma schema, Django model or TypeORM entity and marks every column a subject access request has to reach - with the GDPR article each finding hangs on. 13 rules, line numbers, no account.">5<script defer data-auto-init src="https://cdn.jsdelivr.net/npm/@polar-sh/checkout@latest/dist/embed.global.js"></script>6<style>7:root{color-scheme:light dark}8body{margin:0;font:16px/1.6 system-ui,-apple-system,"Segoe UI",sans-serif;9 background:#fbfbfa;color:#16161d}10@media(prefers-color-scheme:dark){body{background:#14141a;color:#e8e8ea}}11.wrap{max-width:880px;margin:0 auto;padding:28px 20px 64px}12h1{font-size:1.7rem;line-height:1.25;margin:.2em 0 .3em}13p.sub{margin:0 0 1.4em;opacity:.8}14textarea{width:100%;min-height:230px;font:13px/1.5 ui-monospace,Menlo,Consolas,monospace;15 padding:12px;border:1px solid #c9c9d1;border-radius:10px;background:transparent;color:inherit;16 box-sizing:border-box}17button{font:600 15px/1 inherit;padding:12px 22px;border:0;border-radius:10px;18 background:#1d4ed8;color:#fff;cursor:pointer;margin:12px 0}19button:hover{background:#1a43bd}20table{border-collapse:collapse;width:100%;margin-top:14px;font-size:14px}21th,td{border-bottom:1px solid #d8d8e0;padding:8px 6px;text-align:left;vertical-align:top}22th{font-weight:600;opacity:.75}23td.n{width:64px;font:13px ui-monospace,monospace;opacity:.7}24.ok{padding:14px;border-radius:10px;background:#e8f5ec;color:#14532d;margin-top:14px}25@media(prefers-color-scheme:dark){.ok{background:#15321f;color:#bbf7d0}}26.next{margin-top:30px;padding:18px;border:1px solid #d8d8e0;border-radius:12px}27.next h2{font-size:1.05rem;margin:0 0 .5em}28.next ul{margin:.3em 0 1em 1.1em;padding:0}29</style>30<div class="wrap">31<h1>Personal Data Map: find every column a GDPR subject access request has to reach</h1>32<p class="sub">Reads a migration, Prisma schema, Django model or TypeORM entity and marks every column a subject access request has to reach - with the GDPR article each finding hangs on. 13 rules, line numbers, no account.</p>33<textarea id="src" placeholder="Paste a migration, Prisma schema, Django model or TypeORM entity here">-- 014_patients.sql (Postgres, EU health app)34CREATE TABLE users (35 id BIGSERIAL PRIMARY KEY,36 email VARCHAR(255) NOT NULL UNIQUE,37 full_name VARCHAR(120),38 date_of_birth DATE,39 national_id VARCHAR(20),40 password VARCHAR(72) NOT NULL,41 ip_address INET,42 stripe_customer_id VARCHAR(40),43 is_deleted BOOLEAN DEFAULT false,44 created_at TIMESTAMPTZ DEFAULT now()45);46 47CREATE TABLE health_records (48 id BIGSERIAL PRIMARY KEY,49 user_id BIGINT REFERENCES users(id),50 diagnosis TEXT,51 notes TEXT,52 latitude NUMERIC(9,6),53 longitude NUMERIC(9,6)54);55 56CREATE TABLE users_audit (57 id BIGSERIAL PRIMARY KEY,58 user_id BIGINT,59 email VARCHAR(255),60 iban VARCHAR(34),61 changed_at TIMESTAMPTZ62);63</textarea>64<button id="go">Map this schema</button>65<div id="out"></div>66<div class="next"><h2>In the full version</h2><ul><li>Every migration and model in the repository</li><li>Export the map as CSV, JSON or HTML</li><li>JSON your pipeline can fail on</li><li><a href="https://getreadystack.com" data-polar-checkout data-polar-checkout-theme="light">Get the full version</a></li></ul></div>67</div>68<script>69var RULES=[{"pattern": "(?:(?:(?<![A-Za-z0-9_])(?:SEXUAL_ORIENTATION|sexual-orientation|sexual_orientation|POLITICAL_OPINION|political-opinion|political_opinion|sexualOrientation|politicalOpinion|CRIMINAL_RECORD|criminal-record|criminal_record|POLITICAL_VIEW|criminalRecord|political-view|political_view|politicalView|UNION_MEMBER|union-member|union_member|TRADE_UNION|trade-union|trade_union|unionMember|CONVICTION|conviction|tradeUnion|BIOMETRIC|DISABILIT|ETHNICITY|RELIGIOUS|biometric|disabilit|ethnicity|religious|RELIGION|SEX_LIFE|religion|sex-life|sex_life|DIAGNOS|GENETIC|MEDICAL|diagnos|genetic|medical|sexLife|ETHNIC|HEALTH|RACIAL|ethnic|health|racial)|(?<=[a-z0-9])(?:SexualOrientation|PoliticalOpinion|CriminalRecord|PoliticalView|UnionMember|Conviction|TradeUnion|Biometric|Disabilit|Ethnicity|Religious|Religion|Diagnos|Genetic|Medical|SexLife|Ethnic|Health|Racial))[A-Za-z0-9_]{0,24})(?:\\s*:\\s*[A-Za-z_$\\[]|\\s*=\\s*(?:models\\.|db\\.Column|Column\\(|sa\\.Column|mapped_column)|\\s*@Column|\\s+[A-Za-z_]*(?:[Vv][Aa][Rr][Cc][Hh][Aa][Rr]|[Nn][Vv][Aa][Rr][Cc][Hh][Aa][Rr]|[Tt][Ee][Xx][Tt]|[Cc][Hh][Aa][Rr]|[Ss][Tt][Rr][Ii][Nn][Gg]|[Jj][Ss][Oo][Nn]|[Bb][Oo][Oo][Ll]|[Ii][Nn][Tt]|[Ss][Ee][Rr][Ii][Aa][Ll]|[Dd][Aa][Tt][Ee]|[Tt][Ii][Mm][Ee]|[Dd][Ee][Cc][Ii][Mm][Aa][Ll]|[Nn][Uu][Mm][Ee][Rr][Ii][Cc]|[Uu][Uu][Ii][Dd]|[Bb][Ll][Oo][Bb]|[Bb][Yy][Tt][Ee][Aa]|[Ee][Nn][Uu][Mm]|[Ii][Nn][Ee][Tt]|[Cc][Ii][Tt][Ee][Xx][Tt]|[Mm][Oo][Nn][Ee][Yy]|[Ff][Ll][Oo][Aa][Tt]|[Rr][Ee][Aa][Ll]))", "flags": "", "message": "Art. 9 special-category data. It needs an Art. 9(2) condition on top of your Art. 6 legal basis, and it pushes this table over the Art. 35 DPIA threshold.", "sev": "error", "fix": "Record the Art. 9(2) condition beside this column and run a DPIA."}, {"pattern": "(?:(?:(?<![A-Za-z0-9_])(?:NATIONAL_INSURANCE|national-insurance|national_insurance|nationalInsurance|DRIVERS_LICENCE|DRIVERS_LICENSE|SOCIAL_SECURITY|TAX_FILE_NUMBER|drivers-licence|drivers-license|drivers_licence|drivers_license|social-security|social_security|tax-file-number|tax_file_number|DRIVER_LICENCE|DRIVER_LICENSE|driver-licence|driver-license|driver_licence|driver_license|driversLicence|driversLicense|socialSecurity|driverLicence|driverLicense|taxFileNumber|NATIONAL_ID|national-id|national_id|nationalId|PASSPORT|passport)|(?<=[a-z0-9])(?:NationalInsurance|DriversLicence|DriversLicense|SocialSecurity|DriverLicence|DriverLicense|TaxFileNumber|NationalId|Passport))[A-Za-z0-9_]{0,24}|(?:(?<![A-Za-z0-9_])(?:NINO|nino|SSN|ssn)|(?<=[a-z0-9])(?:Nino|Ssn))(?:[_-]?(?:id|no|num|number|code|value)|Id|No|Num|Number|Code|Value)?)(?:\\s*:\\s*[A-Za-z_$\\[]|\\s*=\\s*(?:models\\.|db\\.Column|Column\\(|sa\\.Column|mapped_column)|\\s*@Column|\\s+[A-Za-z_]*(?:[Vv][Aa][Rr][Cc][Hh][Aa][Rr]|[Nn][Vv][Aa][Rr][Cc][Hh][Aa][Rr]|[Tt][Ee][Xx][Tt]|[Cc][Hh][Aa][Rr]|[Ss][Tt][Rr][Ii][Nn][Gg]|[Jj][Ss][Oo][Nn]|[Bb][Oo][Oo][Ll]|[Ii][Nn][Tt]|[Ss][Ee][Rr][Ii][Aa][Ll]|[Dd][Aa][Tt][Ee]|[Tt][Ii][Mm][Ee]|[Dd][Ee][Cc][Ii][Mm][Aa][Ll]|[Nn][Uu][Mm][Ee][Rr][Ii][Cc]|[Uu][Uu][Ii][Dd]|[Bb][Ll][Oo][Bb]|[Bb][Yy][Tt][Ee][Aa]|[Ee][Nn][Uu][Mm]|[Ii][Nn][Ee][Tt]|[Cc][Ii][Tt][Ee][Xx][Tt]|[Mm][Oo][Nn][Ee][Yy]|[Ff][Ll][Oo][Aa][Tt]|[Rr][Ee][Aa][Ll]))", "flags": "", "message": "National identifier. Art. 87 lets each Member State restrict its processing, and a breach of it raises the Art. 34 duty to tell the person directly.", "sev": "error", "fix": "Store a salted hash or an internal id, and keep the raw number out of joins."}, {"pattern": "(?:(?:(?<![A-Za-z0-9_])(?:CLIENT_SECRET|REFRESH_TOKEN|SESSION_TOKEN|client-secret|client_secret|refresh-token|refresh_token|session-token|session_token|ACCESS_TOKEN|access-token|access_token|clientSecret|refreshToken|sessionToken|PRIVATE_KEY|accessToken|private-key|private_key|privateKey|PASSWORD|password|API_KEY|api-key|api_key|PASSWD|SECRET|apiKey|passwd|secret)|(?<=[a-z0-9])(?:ClientSecret|RefreshToken|SessionToken|AccessToken|PrivateKey|Password|ApiKey|Passwd|Secret))(?![_-]?(?:[Hh]ash|HASH|[Dd]igest|[Bb]crypt|[Aa]rgon|[Ss]crypt|sha\\d|[Ee]ncrypted|[Ee]nc|[Kk]ms|KMS|[Vv]ault)\\b)[A-Za-z0-9_]{0,24}|(?:(?<![A-Za-z0-9_])(?:APIKEY|apikey|PWD|pwd)|(?<=[a-z0-9])(?:Apikey|Pwd))(?![_-]?(?:[Hh]ash|HASH|[Dd]igest|[Bb]crypt|[Aa]rgon|[Ss]crypt|sha\\d|[Ee]ncrypted|[Ee]nc|[Kk]ms|KMS|[Vv]ault)\\b)(?:[_-]?(?:id|no|num|number|code|value)|Id|No|Num|Number|Code|Value)?)(?:\\s*:\\s*[A-Za-z_$\\[]|\\s*=\\s*(?:models\\.|db\\.Column|Column\\(|sa\\.Column|mapped_column)|\\s*@Column|\\s+[A-Za-z_]*(?:[Vv][Aa][Rr][Cc][Hh][Aa][Rr]|[Nn][Vv][Aa][Rr][Cc][Hh][Aa][Rr]|[Tt][Ee][Xx][Tt]|[Cc][Hh][Aa][Rr]|[Ss][Tt][Rr][Ii][Nn][Gg]|[Jj][Ss][Oo][Nn]|[Bb][Oo][Oo][Ll]|[Ii][Nn][Tt]|[Ss][Ee][Rr][Ii][Aa][Ll]|[Dd][Aa][Tt][Ee]|[Tt][Ii][Mm][Ee]|[Dd][Ee][Cc][Ii][Mm][Aa][Ll]|[Nn][Uu][Mm][Ee][Rr][Ii][Cc]|[Uu][Uu][Ii][Dd]|[Bb][Ll][Oo][Bb]|[Bb][Yy][Tt][Ee][Aa]|[Ee][Nn][Uu][Mm]|[Ii][Nn][Ee][Tt]|[Cc][Ii][Tt][Ee][Xx][Tt]|[Mm][Oo][Nn][Ee][Yy]|[Ff][Ll][Oo][Aa][Tt]|[Rr][Ee][Aa][Ll]))", "flags": "", "message": "Credential under a plain name. Art. 32 requires state-of-the-art protection, and a DSAR export under Art. 15 must never return this column.", "sev": "error", "fix": "Rename to <name>_hash and store a derived value, or move it to a secret store."}, {"pattern": "(?:[Cc][Rr][Ee][Aa][Tt][Ee]\\s+(?:[Oo][Rr]\\s+[Rr][Ee][Pp][Ll][Aa][Cc][Ee]\\s+)?[Tt][Aa][Bb][Ll][Ee]|^\\s*[Mm][Oo][Dd][Ee][Ll]\\s+|^\\s*[Cc][Ll][Aa][Ss][Ss]\\s+)[^\\n]*(?:[Aa][Uu][Dd][Ii][Tt]|[Hh][Ii][Ss][Tt][Oo][Rr][Yy]|[Aa][Rr][Cc][Hh][Ii][Vv][Ee]|[Bb][Aa][Cc][Kk][Uu][Pp]|[Ss][Hh][Aa][Dd][Oo][Ww]|[Ss][Nn][Aa][Pp][Ss][Hh][Oo][Tt]|[Jj][Oo][Uu][Rr][Nn][Aa][Ll]|[Ee][Vv][Ee][Nn][Tt][Ll][Oo][Gg]|_[Oo][Ll][Dd]|_[Cc][Oo][Pp][Yy]|_[Ll][Oo][Gg])", "flags": "", "message": "Copy table. Art. 17 erasure and Art. 15 access have to reach the copies too - this is where most incomplete DSARs are found, not in the main table.", "sev": "error"}, {"pattern": "[Rr][Ee][Ff][Ee][Rr][Ee][Nn][Cc][Ee][Ss]\\s+[`\"\\[]?\\w*(?:[Uu][Ss][Ee][Rr]|[Cc][Uu][Ss][Tt][Oo][Mm][Ee][Rr]|[Pp][Ee][Rr][Ss][Oo][Nn]|[Pp][Aa][Tt][Ii][Ee][Nn][Tt]|[Ee][Mm][Pp][Ll][Oo][Yy][Ee][Ee]|[Mm][Ee][Mm][Bb][Ee][Rr]|[Cc][Ll][Ii][Ee][Nn][Tt]|[Ss][Uu][Bb][Ss][Cc][Rr][Ii][Bb][Ee][Rr]|[Cc][Oo][Nn][Tt][Aa][Cc][Tt])\\w*[`\"\\]]?\\s*\\([^)]*\\)(?![^\\n]*[Oo][Nn]\\s+[Dd][Ee][Ll][Ee][Tt][Ee])", "flags": "", "message": "Foreign key to the subject table with no ON DELETE rule on this line. Erase the person and this row survives, so the Art. 17 erasure is incomplete.", "sev": "warn"}, {"pattern": "(?:(?:(?<![A-Za-z0-9_])(?:MOBILE_NUMBER|mobile-number|mobile_number|ADDRESS_LINE|address-line|address_line|mobileNumber|MAIDEN_NAME|POSTAL_CODE|addressLine|maiden-name|maiden_name|postal-code|postal_code|FIRST_NAME|GIVEN_NAME|first-name|first_name|given-name|given_name|maidenName|postalCode|FULL_NAME|LAST_NAME|TELEPHONE|firstName|full-name|full_name|givenName|last-name|last_name|telephone|POSTCODE|ZIP_CODE|fullName|lastName|postcode|zip-code|zip_code|SURNAME|surname|zipCode|E_MAIL|MSISDN|STREET|e-mail|e_mail|msisdn|street|EMAIL|PHONE|eMail|email|phone)|(?<=[a-z0-9])(?:MobileNumber|AddressLine|MaidenName|PostalCode|FirstName|GivenName|Telephone|FullName|LastName|Postcode|Surname|ZipCode|Msisdn|Street|EMail|Email|Phone))[A-Za-z0-9_]{0,24})(?:\\s*:\\s*[A-Za-z_$\\[]|\\s*=\\s*(?:models\\.|db\\.Column|Column\\(|sa\\.Column|mapped_column)|\\s*@Column|\\s+[A-Za-z_]*(?:[Vv][Aa][Rr][Cc][Hh][Aa][Rr]|[Nn][Vv][Aa][Rr][Cc][Hh][Aa][Rr]|[Tt][Ee][Xx][Tt]|[Cc][Hh][Aa][Rr]|[Ss][Tt][Rr][Ii][Nn][Gg]|[Jj][Ss][Oo][Nn]|[Bb][Oo][Oo][Ll]|[Ii][Nn][Tt]|[Ss][Ee][Rr][Ii][Aa][Ll]|[Dd][Aa][Tt][Ee]|[Tt][Ii][Mm][Ee]|[Dd][Ee][Cc][Ii][Mm][Aa][Ll]|[Nn][Uu][Mm][Ee][Rr][Ii][Cc]|[Uu][Uu][Ii][Dd]|[Bb][Ll][Oo][Bb]|[Bb][Yy][Tt][Ee][Aa]|[Ee][Nn][Uu][Mm]|[Ii][Nn][Ee][Tt]|[Cc][Ii][Tt][Ee][Xx][Tt]|[Mm][Oo][Nn][Ee][Yy]|[Ff][Ll][Oo][Aa][Tt]|[Rr][Ee][Aa][Ll]))", "flags": "", "message": "Direct identifier. Every Art. 15 export and every Art. 17 erasure has to reach this column, so the table belongs in your Art. 30 record.", "sev": "info"}, {"pattern": "(?:(?:(?<![A-Za-z0-9_])(?:DATE_OF_BIRTH|date-of-birth|date_of_birth|dateOfBirth|BIRTH_DATE|birth-date|birth_date|AGE_GROUP|AGE_YEARS|BIRTHDATE|age-group|age-years|age_group|age_years|birthDate|birthdate|AGE_BAND|GUARDIAN|IS_MINOR|age-band|ageGroup|ageYears|age_band|guardian|is-minor|is_minor|ageBand|isMinor)|(?<=[a-z0-9])(?:DateOfBirth|BirthDate|Birthdate|AgeGroup|AgeYears|Guardian|AgeBand|IsMinor))[A-Za-z0-9_]{0,24}|(?:(?<![A-Za-z0-9_])(?:AGE|DOB|age|dob)|(?<=[a-z0-9])(?:Age|Dob))(?:[_-]?(?:id|no|num|number|code|value)|Id|No|Num|Number|Code|Value)?)(?:\\s*:\\s*[A-Za-z_$\\[]|\\s*=\\s*(?:models\\.|db\\.Column|Column\\(|sa\\.Column|mapped_column)|\\s*@Column|\\s+[A-Za-z_]*(?:[Vv][Aa][Rr][Cc][Hh][Aa][Rr]|[Nn][Vv][Aa][Rr][Cc][Hh][Aa][Rr]|[Tt][Ee][Xx][Tt]|[Cc][Hh][Aa][Rr]|[Ss][Tt][Rr][Ii][Nn][Gg]|[Jj][Ss][Oo][Nn]|[Bb][Oo][Oo][Ll]|[Ii][Nn][Tt]|[Ss][Ee][Rr][Ii][Aa][Ll]|[Dd][Aa][Tt][Ee]|[Tt][Ii][Mm][Ee]|[Dd][Ee][Cc][Ii][Mm][Aa][Ll]|[Nn][Uu][Mm][Ee][Rr][Ii][Cc]|[Uu][Uu][Ii][Dd]|[Bb][Ll][Oo][Bb]|[Bb][Yy][Tt][Ee][Aa]|[Ee][Nn][Uu][Mm]|[Ii][Nn][Ee][Tt]|[Cc][Ii][Tt][Ee][Xx][Tt]|[Mm][Oo][Nn][Ee][Yy]|[Ff][Ll][Oo][Aa][Tt]|[Rr][Ee][Aa][Ll]))", "flags": "", "message": "Age data. If a user can be under 16 (Art. 8; several Member States set 13), the child cannot give valid consent alone and a parental-consent path has to exist.", "sev": "warn"}, {"pattern": "(?:(?:(?<![A-Za-z0-9_])(?:ADVERTISING_ID|advertising-id|advertising_id|advertisingId|FINGERPRINT|REMOTE_ADDR|fingerprint|remote-addr|remote_addr|IP_ADDRESS|USER_AGENT|VISITOR_ID|ip-address|ip_address|remoteAddr|user-agent|user_agent|visitor-id|visitor_id|COOKIE_ID|DEVICE_ID|cookie-id|cookie_id|device-id|device_id|ipAddress|userAgent|visitorId|cookieId|deviceId|IP_ADDR|ip-addr|ip_addr|ipAddr)|(?<=[a-z0-9])(?:AdvertisingId|Fingerprint|RemoteAddr|IpAddress|UserAgent|VisitorId|CookieId|DeviceId|IpAddr))[A-Za-z0-9_]{0,24}|(?:(?<![A-Za-z0-9_])(?:GAID|IDFA|gaid|idfa)|(?<=[a-z0-9])(?:Gaid|Idfa))(?:[_-]?(?:id|no|num|number|code|value)|Id|No|Num|Number|Code|Value)?)(?:\\s*:\\s*[A-Za-z_$\\[]|\\s*=\\s*(?:models\\.|db\\.Column|Column\\(|sa\\.Column|mapped_column)|\\s*@Column|\\s+[A-Za-z_]*(?:[Vv][Aa][Rr][Cc][Hh][Aa][Rr]|[Nn][Vv][Aa][Rr][Cc][Hh][Aa][Rr]|[Tt][Ee][Xx][Tt]|[Cc][Hh][Aa][Rr]|[Ss][Tt][Rr][Ii][Nn][Gg]|[Jj][Ss][Oo][Nn]|[Bb][Oo][Oo][Ll]|[Ii][Nn][Tt]|[Ss][Ee][Rr][Ii][Aa][Ll]|[Dd][Aa][Tt][Ee]|[Tt][Ii][Mm][Ee]|[Dd][Ee][Cc][Ii][Mm][Aa][Ll]|[Nn][Uu][Mm][Ee][Rr][Ii][Cc]|[Uu][Uu][Ii][Dd]|[Bb][Ll][Oo][Bb]|[Bb][Yy][Tt][Ee][Aa]|[Ee][Nn][Uu][Mm]|[Ii][Nn][Ee][Tt]|[Cc][Ii][Tt][Ee][Xx][Tt]|[Mm][Oo][Nn][Ee][Yy]|[Ff][Ll][Oo][Aa][Tt]|[Rr][Ee][Aa][Ll]))", "flags": "", "message": "Online identifier. Recital 30 and CJEU C-582/14 (Breyer) make a dynamic IP personal data when you can identify the user - these are the columns teams leave out of the Art. 30 record.", "sev": "warn"}, {"pattern": "(?:(?:(?<![A-Za-z0-9_])(?:COORDINATES|GEOLOCATION|coordinates|geolocation|GEO_POINT|LONGITUDE|geo-point|geo_point|longitude|GEO_HASH|LATITUDE|geo-hash|geoPoint|geo_hash|latitude|GEOHASH|geoHash|geohash|GPS|gps)|(?<=[a-z0-9])(?:Coordinates|Geolocation|Longitude|GeoPoint|Latitude|GeoHash|Geohash|Gps))[A-Za-z0-9_]{0,24}|(?:(?<![A-Za-z0-9_])(?:LAT|LNG|LON|lat|lng|lon)|(?<=[a-z0-9])(?:Lat|Lng|Lon))(?:[_-]?(?:id|no|num|number|code|value)|Id|No|Num|Number|Code|Value)?)(?:\\s*:\\s*[A-Za-z_$\\[]|\\s*=\\s*(?:models\\.|db\\.Column|Column\\(|sa\\.Column|mapped_column)|\\s*@Column|\\s+[A-Za-z_]*(?:[Vv][Aa][Rr][Cc][Hh][Aa][Rr]|[Nn][Vv][Aa][Rr][Cc][Hh][Aa][Rr]|[Tt][Ee][Xx][Tt]|[Cc][Hh][Aa][Rr]|[Ss][Tt][Rr][Ii][Nn][Gg]|[Jj][Ss][Oo][Nn]|[Bb][Oo][Oo][Ll]|[Ii][Nn][Tt]|[Ss][Ee][Rr][Ii][Aa][Ll]|[Dd][Aa][Tt][Ee]|[Tt][Ii][Mm][Ee]|[Dd][Ee][Cc][Ii][Mm][Aa][Ll]|[Nn][Uu][Mm][Ee][Rr][Ii][Cc]|[Uu][Uu][Ii][Dd]|[Bb][Ll][Oo][Bb]|[Bb][Yy][Tt][Ee][Aa]|[Ee][Nn][Uu][Mm]|[Ii][Nn][Ee][Tt]|[Cc][Ii][Tt][Ee][Xx][Tt]|[Mm][Oo][Nn][Ee][Yy]|[Ff][Ll][Oo][Aa][Tt]|[Rr][Ee][Aa][Ll]))", "flags": "", "message": "Precise location. Continuous location tied to an identified person is systematic monitoring and counts toward the Art. 35 DPIA threshold.", "sev": "warn"}, {"pattern": "(?:(?:(?<![A-Za-z0-9_])(?:MESSAGE_BODY|message-body|message_body|DESCRIPTION|description|messageBody|BIOGRAPHY|FREE_TEXT|biography|free-text|free_text|FEEDBACK|feedback|freeText|COMMENT|REMARKS|comment|remarks|REASON|reason|NOTES|notes)|(?<=[a-z0-9])(?:Description|MessageBody|Biography|Feedback|FreeText|Comment|Remarks|Reason|Notes))[A-Za-z0-9_]{0,24}|(?:(?<![A-Za-z0-9_])(?:NOTE|note|BIO|bio)|(?<=[a-z0-9])(?:Note|Bio))(?:[_-]?(?:id|no|num|number|code|value)|Id|No|Num|Number|Code|Value)?)(?:\\s*:\\s*[A-Za-z_$\\[]|\\s*=\\s*(?:models\\.|db\\.Column|Column\\(|sa\\.Column|mapped_column)|\\s*@Column|\\s+[A-Za-z_]*(?:[Vv][Aa][Rr][Cc][Hh][Aa][Rr]|[Nn][Vv][Aa][Rr][Cc][Hh][Aa][Rr]|[Tt][Ee][Xx][Tt]|[Cc][Hh][Aa][Rr]|[Ss][Tt][Rr][Ii][Nn][Gg]|[Jj][Ss][Oo][Nn]|[Bb][Oo][Oo][Ll]|[Ii][Nn][Tt]|[Ss][Ee][Rr][Ii][Aa][Ll]|[Dd][Aa][Tt][Ee]|[Tt][Ii][Mm][Ee]|[Dd][Ee][Cc][Ii][Mm][Aa][Ll]|[Nn][Uu][Mm][Ee][Rr][Ii][Cc]|[Uu][Uu][Ii][Dd]|[Bb][Ll][Oo][Bb]|[Bb][Yy][Tt][Ee][Aa]|[Ee][Nn][Uu][Mm]|[Ii][Nn][Ee][Tt]|[Cc][Ii][Tt][Ee][Xx][Tt]|[Mm][Oo][Nn][Ee][Yy]|[Ff][Ll][Oo][Aa][Tt]|[Rr][Ee][Aa][Ll]))", "flags": "", "message": "Free-text column. An Art. 15 request covers whatever a colleague typed here and you cannot predict it, so this field has to be searched rather than skipped.", "sev": "info"}, {"pattern": "(?:(?:(?<![A-Za-z0-9_])(?:STRIPE_CUSTOMER|stripe-customer|stripe_customer|stripeCustomer|EXTERNAL_CRM|external-crm|external_crm|externalCrm|SALESFORCE|SEGMENT_ID|salesforce|segment-id|segment_id|AMPLITUDE|MAILCHIMP|amplitude|mailchimp|segmentId|INTERCOM|MIXPANEL|intercom|mixpanel|HUBSPOT|KLAVIYO|POSTHOG|ZENDESK|hubspot|klaviyo|posthog|zendesk|BRAZE|braze)|(?<=[a-z0-9])(?:StripeCustomer|ExternalCrm|Salesforce|Amplitude|Mailchimp|SegmentId|Intercom|Mixpanel|Hubspot|Klaviyo|Posthog|Zendesk|Braze))[A-Za-z0-9_]{0,24})(?:\\s*:\\s*[A-Za-z_$\\[]|\\s*=\\s*(?:models\\.|db\\.Column|Column\\(|sa\\.Column|mapped_column)|\\s*@Column|\\s+[A-Za-z_]*(?:[Vv][Aa][Rr][Cc][Hh][Aa][Rr]|[Nn][Vv][Aa][Rr][Cc][Hh][Aa][Rr]|[Tt][Ee][Xx][Tt]|[Cc][Hh][Aa][Rr]|[Ss][Tt][Rr][Ii][Nn][Gg]|[Jj][Ss][Oo][Nn]|[Bb][Oo][Oo][Ll]|[Ii][Nn][Tt]|[Ss][Ee][Rr][Ii][Aa][Ll]|[Dd][Aa][Tt][Ee]|[Tt][Ii][Mm][Ee]|[Dd][Ee][Cc][Ii][Mm][Aa][Ll]|[Nn][Uu][Mm][Ee][Rr][Ii][Cc]|[Uu][Uu][Ii][Dd]|[Bb][Ll][Oo][Bb]|[Bb][Yy][Tt][Ee][Aa]|[Ee][Nn][Uu][Mm]|[Ii][Nn][Ee][Tt]|[Cc][Ii][Tt][Ee][Xx][Tt]|[Mm][Oo][Nn][Ee][Yy]|[Ff][Ll][Oo][Aa][Tt]|[Rr][Ee][Aa][Ll]))", "flags": "", "message": "This column proves personal data left for a processor. That processor has to be named in your Art. 30(1) record and covered by an Art. 28 contract.", "sev": "warn"}, {"pattern": "(?:(?:(?<![A-Za-z0-9_])(?:DELETED_FLAG|deleted-flag|deleted_flag|ARCHIVED_AT|IS_ARCHIVED|SOFT_DELETE|archived-at|archived_at|deletedFlag|is-archived|is_archived|soft-delete|soft_delete|DELETED_AT|IS_DELETED|IS_REMOVED|archivedAt|deleted-at|deleted_at|is-deleted|is-removed|isArchived|is_deleted|is_removed|softDelete|deletedAt|isDeleted|isRemoved)|(?<=[a-z0-9])(?:DeletedFlag|ArchivedAt|IsArchived|SoftDelete|DeletedAt|IsDeleted|IsRemoved))[A-Za-z0-9_]{0,24})(?:\\s*:\\s*[A-Za-z_$\\[]|\\s*=\\s*(?:models\\.|db\\.Column|Column\\(|sa\\.Column|mapped_column)|\\s*@Column|\\s+[A-Za-z_]*(?:[Vv][Aa][Rr][Cc][Hh][Aa][Rr]|[Nn][Vv][Aa][Rr][Cc][Hh][Aa][Rr]|[Tt][Ee][Xx][Tt]|[Cc][Hh][Aa][Rr]|[Ss][Tt][Rr][Ii][Nn][Gg]|[Jj][Ss][Oo][Nn]|[Bb][Oo][Oo][Ll]|[Ii][Nn][Tt]|[Ss][Ee][Rr][Ii][Aa][Ll]|[Dd][Aa][Tt][Ee]|[Tt][Ii][Mm][Ee]|[Dd][Ee][Cc][Ii][Mm][Aa][Ll]|[Nn][Uu][Mm][Ee][Rr][Ii][Cc]|[Uu][Uu][Ii][Dd]|[Bb][Ll][Oo][Bb]|[Bb][Yy][Tt][Ee][Aa]|[Ee][Nn][Uu][Mm]|[Ii][Nn][Ee][Tt]|[Cc][Ii][Tt][Ee][Xx][Tt]|[Mm][Oo][Nn][Ee][Yy]|[Ff][Ll][Oo][Aa][Tt]|[Rr][Ee][Aa][Ll]))", "flags": "", "message": "Soft delete. Art. 17 erasure is not a flag - the row still holds the person and read replicas still serve it. Name the hard-delete or anonymise job that follows.", "sev": "warn"}, {"pattern": "(?:(?:(?<![A-Za-z0-9_])(?:ACCOUNT_NUMBER|ROUTING_NUMBER|account-number|account_number|routing-number|routing_number|accountNumber|routingNumber|BANK_ACCOUNT|bank-account|bank_account|CARD_NUMBER|bankAccount|card-number|card_number|CARDHOLDER|cardNumber|cardholder|SORT_CODE|sort-code|sort_code|sortCode)|(?<=[a-z0-9])(?:AccountNumber|RoutingNumber|BankAccount|CardNumber|Cardholder|SortCode))[A-Za-z0-9_]{0,24}|(?:(?<![A-Za-z0-9_])(?:SWIFT|swift|IBAN|iban|BIC|CVC|CVV|bic|cvc|cvv)|(?<=[a-z0-9])(?:Swift|Iban|Bic|Cvc|Cvv))(?:[_-]?(?:id|no|num|number|code|value)|Id|No|Num|Number|Code|Value)?)(?:\\s*:\\s*[A-Za-z_$\\[]|\\s*=\\s*(?:models\\.|db\\.Column|Column\\(|sa\\.Column|mapped_column)|\\s*@Column|\\s+[A-Za-z_]*(?:[Vv][Aa][Rr][Cc][Hh][Aa][Rr]|[Nn][Vv][Aa][Rr][Cc][Hh][Aa][Rr]|[Tt][Ee][Xx][Tt]|[Cc][Hh][Aa][Rr]|[Ss][Tt][Rr][Ii][Nn][Gg]|[Jj][Ss][Oo][Nn]|[Bb][Oo][Oo][Ll]|[Ii][Nn][Tt]|[Ss][Ee][Rr][Ii][Aa][Ll]|[Dd][Aa][Tt][Ee]|[Tt][Ii][Mm][Ee]|[Dd][Ee][Cc][Ii][Mm][Aa][Ll]|[Nn][Uu][Mm][Ee][Rr][Ii][Cc]|[Uu][Uu][Ii][Dd]|[Bb][Ll][Oo][Bb]|[Bb][Yy][Tt][Ee][Aa]|[Ee][Nn][Uu][Mm]|[Ii][Nn][Ee][Tt]|[Cc][Ii][Tt][Ee][Xx][Tt]|[Mm][Oo][Nn][Ee][Yy]|[Ff][Ll][Oo][Aa][Tt]|[Rr][Ee][Aa][Ll]))", "flags": "", "message": "Payment data. PCI DSS forbids storing CVV at all, and card data widens both the Art. 32 duty and what you must report within 72 hours under Art. 33.", "sev": "warn"}];70function scan(t){var L=String(t).split(/\r?\n/),h=[],i,r,re;71 for(i=0;i<L.length;i++){for(var k=0;k<RULES.length;k++){r=RULES[k];72 try{re=new RegExp(r.pattern,r.flags||'')}catch(e){continue}73 if(re.test(L[i]))h.push({line:i+1,msg:r.message})}}74 return h}75document.getElementById('go').onclick=function(){76 var t=document.getElementById('src').value,o=document.getElementById('out');77 if(!t.trim()){o.innerHTML='';return}78 var h=scan(t);79 if(!h.length){o.innerHTML='<div class="ok">No personal-data columns matched in this file.</div>';return}80 var s='<table><tr><th>Line</th><th>Finding</th></tr>';81 for(var i=0;i<h.length;i++){s+='<tr><td class="n">'+h[i].line+'</td><td>'+82 String(h[i].msg).replace(/&/g,'&').replace(/</g,'<')+'</td></tr>'}83 o.innerHTML=s+'</table>'};84document.getElementById('go').click();85</script>86</html>87 88<section data-yj-alert="rule" style="margin:1rem auto 1.5rem;max-width:46rem;padding:1.1rem 1.25rem;border:1px solid #d9dee6;border-radius:12px;background:#fbfcfd;font:15px/1.55 system-ui,-apple-system,Segoe UI,sans-serif"><b style="font-size:1.05rem">Get one email when this rule changes</b><div style="opacity:.78;margin:.35rem 0 .8rem">We watch the regulation and vendor sources behind Personal Data Map: find every column a GDPR subject access request has to reach every day. When a rule changes, you get a single email with what changed and the updated check. No newsletter.</div><div style="display:flex;gap:8px;flex-wrap:wrap"><input id="yjae" type="email" placeholder="you@example.com" style="flex:1;min-width:210px;padding:11px 13px;border:1px solid rgba(140,150,165,.34);border-radius:8px;background:#fff;color:inherit"><button id="yjab" style="padding:11px 18px;border-radius:8px;border:0;cursor:pointer;background:#0f7b3f;color:#fff;font:600 15px system-ui,sans-serif">Notify me</button></div><div id="yjao" hidden style="margin-top:.6rem;color:#0f7b3f">You're on the list. One email when it changes.</div><script>(function(){var b=document.getElementById("yjab"),e=document.getElementById("yjae");if(!b||!e)return;b.addEventListener("click",function(){var v=e.value.trim();if(!/.+@.+\..+/.test(v))return;try{localStorage.setItem("yj_em","1");}catch(_){}fetch(location.origin+"/api/subscribe",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({email:v,slug:"personal-data-map-dsar-audit",lang:"en",source:"rule_alert"})}).then(function(r){var o=document.getElementById("yjao");o.hidden=false;if(!r.ok){o.textContent="Could not save - please try again.";}}).catch(function(){var o=document.getElementById("yjao");o.hidden=false;o.textContent="Could not save - please try again.";});});})();</script></section>89 90 91<section data-yj-install="free" style="margin:2rem auto 1rem;max-width:46rem;padding:1.25rem 1.25rem;border:1px solid #cfe3d6;border-radius:12px;background:#f6fbf8;font:16px/1.6 system-ui,-apple-system,Segoe UI,sans-serif"><h2 style="margin:0 0 .4rem;font-size:1.2rem">Install free</h2><p style="margin:0 0 .8rem;color:#41505f">Free for the file open in your editor - no key, no limit. The workspace sweep and the report ask for a key.</p><a href="https://marketplace.visualstudio.com/items?itemName=ReadyStack.personal-data-map-dsar-audit" rel="noopener" data-yj-install-door="mp" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'personal-data-map-dsar-audit',src:'mp',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">VS Code Marketplace</a><a href="https://open-vsx.org/extension/readystack/personal-data-map-dsar-audit" rel="noopener" data-yj-install-door="ovsx" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'personal-data-map-dsar-audit',src:'ovsx',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">Open VSX (Cursor, VSCodium)</a><a href="https://www.npmjs.com/package/@readystack/personal-data-map-dsar-audit" rel="noopener" data-yj-install-door="npm" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'personal-data-map-dsar-audit',src:'npm',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">npm</a><a href="https://www.npmjs.com/package/@readystack/personal-data-map-dsar-audit" rel="noopener" data-yj-install-door="mcp" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'personal-data-map-dsar-audit',src:'mcp',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">MCP server (npx)</a><a href="https://hub.docker.com/r/getreadystack/personal-data-map-dsar-audit" rel="noopener" data-yj-install-door="docker" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'personal-data-map-dsar-audit',src:'docker',path:location.pathname})],{type:'application/json'}))}catch(e){}" style="display:inline-block;margin:0 .5rem .5rem 0;padding:.6rem 1rem;border-radius:8px;background:#0f7b3f;color:#fff;text-decoration:none;font-weight:600">Docker Hub</a><pre style="margin:.5rem 0 0;padding:.6rem .8rem;background:#f3f5f8;border-radius:8px;font-size:.9rem;overflow:auto">ext install ReadyStack.personal-data-map-dsar-audit92npx @readystack/personal-data-map-dsar-audit <file>93npx @readystack/personal-data-map-dsar-audit --mcp</pre><p style="margin:.6rem 0 0;font-size:.92rem;color:#41505f"><a href="https://buy.polar.sh/polar_cl_l6iN1uWt0FwWu7tBsczD0jWpP2vxFM54Wdwqb3KPi1G" rel="nofollow" style="color:#0f7b3f;text-decoration:underline">Team? One key for every ReadyStack linter, 5 seats, $149 once</a></p></section>94<section data-yj-cta="paid" style="margin:3rem auto;max-width:46rem;padding:1.5rem 1.25rem;border:1px solid #d8dee9;border-radius:12px;background:#fbfcfe;font:16px/1.6 system-ui,-apple-system,Segoe UI,sans-serif"><h2 style="margin:0 0 .5rem;font-size:1.25rem">Get the complete version <span class="yj-price">$29</span></h2><p style="margin:.25rem 0 1rem;color:#41505f">This page is the working piece. The full pack has everything below.</p><p style="margin:0 0 1rem;color:#41505f">Reads a migration, Prisma schema, Django model or TypeORM entity and marks every column a subject access request has to reach - with the GDPR article each finding hangs on. 13 rules, line nu</p><p style="margin:0 0 1rem;color:#5b6b7c;font-size:.94rem">An independent EU privacy consultant bills EUR 100-200/hour (market average about EUR 150) and one manual DSAR averages about $1,524 in staff time across 8-12 hours.</p><a href="https://buy.polar.sh/polar_cl_BXPHG1fr7NIyCwKd6UwVuUit0HVpNLllIqENN46kmIz" rel="nofollow" style="display:inline-block;padding:.7rem 1.3rem;border-radius:8px;background:#1b64f2;color:#fff;text-decoration:none;font-weight:600">Buy the full version — $29</a></section>95<!--yj-share-s101--><div style="margin:28px 0 8px;text-align:center"><button id="yjcp" type="button" style="font:inherit;padding:10px 18px;border:1px solid #8884;border-radius:10px;background:transparent;cursor:pointer">Copy link</button><button id="yjimg" type="button" style="font:inherit;padding:10px 18px;margin-left:8px;border:1px solid #8884;border-radius:10px;background:transparent;cursor:pointer">Save result as image</button></div><script>(function(){var b=document.getElementById("yjimg");if(!b)return;b.addEventListener("click",function(){var o=document.getElementById("out");var txt=(o&&(o.innerText||o.textContent)||"").trim();if(!txt){b.textContent="(no result yet)";setTimeout(function(){b.textContent="Save result as image";},1500);return;}var lines=txt.split(String.fromCharCode(10)).filter(function(x){return x.trim();}).slice(0,10);var W=1200,P=56,c=document.createElement("canvas");c.width=W;var lh=38,H=P*2+70+lines.length*lh+60;c.height=H;var x=c.getContext("2d");x.fillStyle="#1a1512";x.fillRect(0,0,W,H);x.fillStyle="#f3e9d2";x.font="600 34px system-ui,Segoe UI,Arial";var t=(document.title||"").split("|")[0].trim();x.fillText(t.slice(0,54),P,P+34);x.font="22px ui-monospace,Menlo,Consolas,monospace";x.fillStyle="#e8dcc4";for(var i=0;i<lines.length;i++){x.fillText(lines[i].slice(0,88),P,P+70+i*lh+22);}x.font="600 22px system-ui,Segoe UI,Arial";x.fillStyle="#c9a227";var d=location.hostname.replace("www.","");x.fillText(d,W-P-x.measureText(d).width,H-P+6);c.toBlob(function(bl){var u=URL.createObjectURL(bl),a=document.createElement("a");a.href=u;a.download=(t||"result").replace(/[^a-z0-9]+/gi,"-").toLowerCase()+"-"+d+".png";document.body.appendChild(a);a.click();setTimeout(function(){URL.revokeObjectURL(u);a.remove();},2000);},"image/png");});})();</script><script>(function(){var b=document.getElementById("yjcp");if(!b)return;var t=b.textContent;b.addEventListener("click",function(){var u=location.href.split("#")[0];function d(){b.textContent="Link copied";setTimeout(function(){b.textContent=t;},1800);}if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(u).then(d,function(){prompt("",u);});}else{prompt("",u);}});})();</script><div class="yj-home-find"><div class="yj-lgbar"><a href="/" class="yj-lg on">EN</a><a href="/de/" class="yj-lg">DE</a><a href="/ja/" class="yj-lg">JA</a><a href="/es/" class="yj-lg">ES</a><a href="/pt/" class="yj-lg">PT</a></div><section class="yj-find"><h2>Find a tool</h2><input id="yjq" type="search" autocomplete="off" placeholder="Type a product name, format or task"><div id="yjr"></div></section><style>.yj-lgbar{position:absolute;top:14px;right:18px;display:flex;gap:6px;z-index:60}.yj-lg{font:600 12px/1 system-ui,sans-serif;padding:6px 9px;border-radius:4px;text-decoration:none;color:#8b96a3;border:1px solid rgba(140,150,165,.28)}.yj-lg:hover{color:#e8eaed;border-color:rgba(160,175,195,.6)}.yj-lg.on{color:#0e1116;background:#cfd6de;border-color:#cfd6de}.yj-find{max-width:760px;margin:34px auto 8px;padding:0 20px}.yj-find h2{font:600 20px/1.3 system-ui,sans-serif;margin:0 0 10px}#yjq{width:100%;box-sizing:border-box;padding:13px 15px;font:400 15.5px system-ui,sans-serif;border:1px solid rgba(140,150,165,.34);border-radius:8px;background:rgba(255,255,255,.04);color:inherit}#yjq:focus{outline:none;border-color:#7FD1C4}#yjr{margin-top:10px;display:flex;flex-direction:column;gap:6px}#yjr a{display:flex;justify-content:space-between;gap:12px;padding:11px 14px;border:1px solid rgba(140,150,165,.22);border-radius:6px;text-decoration:none;font:400 14.5px/1.4 system-ui,sans-serif;color:inherit}#yjr a:hover{border-color:#7FD1C4}#yjr .pr{font:600 13px system-ui,sans-serif;color:#5fbf9a;white-space:nowrap}</style><script>(function(){var q=document.getElementById("yjq"),r=document.getElementById("yjr"),D=null;function load(){if(D)return Promise.resolve(D);return fetch("/search-index.json").then(function(x){return x.json();}).then(function(j){D=j;return j;}).catch(function(){D=[];return D;});}function esc(t){return String(t).replace(/[&<>"]/g,function(c){return {"&":"&","<":"<",">":">",'"':"""}[c];});}function run(){var v=q.value.trim().toLowerCase();if(v.length<2){r.innerHTML="";return;}load().then(function(j){var w=v.split(/\s+/),o=[];for(var i=0;i<j.length&&o.length<12;i++){var t=j[i].t.toLowerCase();var ok=true;for(var k=0;k<w.length;k++){if(t.indexOf(w[k])<0){ok=false;break;}}if(ok)o.push(j[i]);}r.innerHTML=o.length?o.map(function(x){return "<a href=\""+esc(x.u)+"\">"+"<span>"+esc(x.t)+"</span>"+(x.p?"<span class=\"pr\">$"+x.p+"</span>":"")+"</a>";}).join(""):"";});}q.addEventListener("input",run);q.addEventListener("focus",load);})();</script></div><section data-yj="yj-facts-v1" style="max-width:920px;margin:48px auto 0;padding:0 20px;font-family:system-ui,sans-serif"><style>.yj-article p{font:400 15.5px/1.7 system-ui,sans-serif;color:#26313b;margin:0 0 12px;max-width:72ch}.yj-article h2,.yj-article h3,.yj-article h4{font:600 17px/1.35 system-ui,sans-serif;color:#1b1f24;margin:22px 0 8px}.yj-article table{border-collapse:collapse;margin:10px 0 16px;font:14px/1.45 system-ui,sans-serif}.yj-article th,.yj-article td{border:1px solid #e2e6ea;padding:6px 10px;text-align:left}.yj-article th{background:#f4f6f8}.yj-article pre{background:#0f1720;color:#e6edf3;padding:12px 14px;border-radius:8px;overflow:auto;font:13px/1.5 ui-monospace,Menlo,monospace}.yj-article code{font:13px ui-monospace,Menlo,monospace;background:#f1f3f5;padding:1px 4px;border-radius:3px}.yj-article pre code{background:none;padding:0}.yj-article ul,.yj-article ol{font:15px/1.6 system-ui,sans-serif;color:#26313b}</style><p style="font:400 13px/1.5 system-ui,sans-serif;color:#6b7680;margin:0 0 14px"><time datetime="2026-09-08">Updated 2026-09-08</time> · ReadyStack</p><script type="application/ld+json">{"@context":"https://schema.org","@type":"WebPage","dateModified":"2026-09-08","author":{"@type":"Organization","name":"ReadyStack"}}</script><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:0 0 6px;color:#1b1f24">Worked example</h2><p style="font:400 14.5px/1.5 system-ui,sans-serif;margin:0 0 16px;color:#5b6570">Real numbers from this tool, line by line.</p><div class="yj-article"><p><img src="https://getreadystack.com/img/promo/sku20608_result_card.jpg" alt="Five columns in one migration will break your next subject access request" loading="lazy" style="max-width:100%;border-radius:6px"></p>96<p>Five columns in a thirty-line Postgres migration will break the next subject access request a Berlin SaaS team receives, and the worst of them is a copy table nobody remembers creating.</p>97<p>Here is the migration. Nothing in it is unusual; every line of it has been written by a careful engineer.</p>98<pre><code>CREATE TABLE users (99 id BIGSERIAL PRIMARY KEY,100 email VARCHAR(255) NOT NULL UNIQUE,101 date_of_birth DATE,102 national_id VARCHAR(20),103 password VARCHAR(72) NOT NULL,104 ip_address INET,105 stripe_customer_id VARCHAR(40),106 is_deleted BOOLEAN DEFAULT false107);108 109CREATE TABLE health_records (110 user_id BIGINT REFERENCES users(id),111 diagnosis TEXT,112 notes TEXT113);114 115CREATE TABLE users_audit (116 user_id BIGINT,117 email VARCHAR(255),118 iban VARCHAR(34)119);</code></pre>120<p>Under Art. 12(3) you have one month from the request, extendable by two more only if you tell the person why inside that first month. Here is what that month runs into.</p>121<p><code>password</code> is stored under a plain name. Art. 32 asks for state-of-the-art protection, and an Art. 15 export must never return it. Rename it to <code>password_hash</code> and store a derived value.</p>122<p><code>diagnosis</code> is Art. 9 special-category data. It needs an Art. 9(2) condition sitting on top of your Art. 6 basis, and it pushes the table over the Art. 35 DPIA threshold.</p>123<p><code>CREATE TABLE users_audit</code> is the expensive one. Erasure under Art. 17 and access under Art. 15 reach the copies too, and incomplete erasure is almost always found here rather than in the main table.</p>124<p><code>REFERENCES users(id)</code> with no ON DELETE rule means the health record survives the user you just deleted. The erasure was not an erasure.</p>125<p><code>is_deleted BOOLEAN</code> is the same failure wearing a different hat. A flag is not erasure. The row still holds the person, and read replicas still serve it. Name the hard-delete or anonymise job that follows the flag, or the flag is a promise you do not keep.</p>126<p>Two more the same file hides: <code>ip_address</code> is personal data once you can identify the user (Recital 30, and the CJEU in Breyer, C-582/14), and <code>stripe_customer_id</code> is proof that this person's data left for a processor who must be named in your Art. 30(1) record.</p>127<p>The obvious move is to paste the file into a chatbot. It reads the one table you gave it. It cannot see the other forty migrations, and it cannot see the missing ON DELETE three files away. On the law it is worse than blind: ask it about the Art. 30 record today and it will hand you the Digital Omnibus 750-employee exemption, which as of September 2026 is a proposal still under negotiation, not law you can rely on.</p>128<p>A manual subject access request averages about $1,524 in staff time, eight to twelve hours of it, and an independent EU privacy consultant bills EUR 100-200 an hour. The map of which columns you actually hold is the cheap part, and it should not be the part you do by reading.</p>129<p>Personal Data Map is a VS Code extension and a free web page that run the same thirteen rules over a schema and return line numbers with the article each finding hangs on. The free side finishes the file you have open. Paste a migration into the page and the answer is on screen before you type anything.</p></div><h2 style="font:600 19px/1.3 system-ui,sans-serif;margin:30px 0 10px;color:#1b1f24">15 seconds — what it actually does</h2><video controls muted playsinline preload="none" poster="/img/promo/sku20608_result_card.jpg" style="width:100%;max-width:360px;border-radius:10px;display:block;margin:0 auto"><source src="/media/sku20608_short.mp4" type="video/mp4"></video><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:34px 0 6px;color:#1b1f24">Questions people ask</h2><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What does Personal Data Map actually do?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">It reads a database migration, Prisma schema, Django model or TypeORM entity and marks every column a GDPR request has to reach. Each finding carries the line number, the severity, and the article it hangs on: Art. 9 special category, Art. 32 credentials, Art. 17 erasure, Art. 30 records. Thirteen rules ship inside.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Who is this for?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Backend and platform engineers who own the migrations and ORM models in an EU or UK product, and who get handed the subject access request when one arrives. It is aimed at teams without a full-time privacy engineer, where the person who wrote the schema is also the person who has to answer for it.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">Why is asking an AI assistant not enough?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">A chatbot reads the one table you paste. It cannot see the other forty migrations, the users_audit copy table, or the missing ON DELETE three files away. On the law it is also stale: ask about the Art. 30 record and it repeats the Digital Omnibus 750-employee exemption, which in September 2026 is still a proposal under negotiation, not law.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What is free and what needs a licence?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Free runs all thirteen rules over the file you have open, or over a selection, with no account and no key: that job finishes. A licence adds a different job, not a bigger one — mapping every migration and model in the repository at once, exporting the map as CSV, JSON or HTML, and writing JSON your CI can fail on.</p></details><details style="border-bottom:1px solid #e6e9ed;padding:10px 0"><summary style="cursor:pointer;font:600 15px/1.4 system-ui,sans-serif;color:#1b1f24">What does the alternative cost?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">An independent EU privacy consultant bills roughly EUR 100-200 an hour, market average about EUR 150, and UK outsourced DPO work starts near GBP 150 an hour. One manual subject access request averages about $1,524 in staff time across eight to twelve hours. The licence is $29 once, with a seven-day full refund.</p></details><script type="application/ld+json" data-yj="yj-faq-ld">{"@context": "https://schema.org", "@type": "FAQPage", "mainEntity": [{"@type": "Question", "name": "What does Personal Data Map actually do?", "acceptedAnswer": {"@type": "Answer", "text": "It reads a database migration, Prisma schema, Django model or TypeORM entity and marks every column a GDPR request has to reach. Each finding carries the line number, the severity, and the article it hangs on: Art. 9 special category, Art. 32 credentials, Art. 17 erasure, Art. 30 records. Thirteen rules ship inside."}}, {"@type": "Question", "name": "Who is this for?", "acceptedAnswer": {"@type": "Answer", "text": "Backend and platform engineers who own the migrations and ORM models in an EU or UK product, and who get handed the subject access request when one arrives. It is aimed at teams without a full-time privacy engineer, where the person who wrote the schema is also the person who has to answer for it."}}, {"@type": "Question", "name": "Why is asking an AI assistant not enough?", "acceptedAnswer": {"@type": "Answer", "text": "A chatbot reads the one table you paste. It cannot see the other forty migrations, the users_audit copy table, or the missing ON DELETE three files away. On the law it is also stale: ask about the Art. 30 record and it repeats the Digital Omnibus 750-employee exemption, which in September 2026 is still a proposal under negotiation, not law."}}, {"@type": "Question", "name": "What is free and what needs a licence?", "acceptedAnswer": {"@type": "Answer", "text": "Free runs all thirteen rules over the file you have open, or over a selection, with no account and no key: that job finishes. A licence adds a different job, not a bigger one — mapping every migration and model in the repository at once, exporting the map as CSV, JSON or HTML, and writing JSON your CI can fail on."}}, {"@type": "Question", "name": "What does the alternative cost?", "acceptedAnswer": {"@type": "Answer", "text": "An independent EU privacy consultant bills roughly EUR 100-200 an hour, market average about EUR 150, and UK outsourced DPO work starts near GBP 150 an hour. One manual subject access request averages about $1,524 in staff time across eight to twelve hours. The licence is $29 once, with a seven-day full refund."}}]}</script><div id="yj-ask" style="margin:36px 0 0;padding:18px 18px 16px;border:1px solid #e2e6ea;border-radius:10px;background:#fbfbfc"><h2 style="font:600 19px/1.3 system-ui,sans-serif;margin:0 0 4px;color:#1b1f24">Ask about this tool</h2><p style="font:400 13.5px/1.5 system-ui,sans-serif;margin:0 0 12px;color:#5b6570">One question, answered by the person who built it. Your email only if you want the answer sent.</p><form onsubmit="return yjAsk(event)" style="display:grid;gap:8px"><textarea name="question" required minlength="10" maxlength="2000" rows="3" placeholder="Your question" style="font:14px/1.5 system-ui,sans-serif;padding:10px;border:1px solid #cfd6dd;border-radius:6px"></textarea><div style="display:flex;gap:8px;flex-wrap:wrap"><input type="email" name="email" placeholder="Email (optional)" style="flex:1;min-width:200px;font:14px system-ui,sans-serif;padding:9px 10px;border:1px solid #cfd6dd;border-radius:6px"><button type="submit" style="font:600 14px system-ui,sans-serif;padding:9px 18px;border:0;border-radius:6px;background:#1b1f24;color:#fff;cursor:pointer">Send</button></div><p id="yj-ask-msg" style="font:13px system-ui,sans-serif;margin:2px 0 0;color:#0a6238;min-height:1em"></p></form><script>function yjAsk(e){e.preventDefault();var f=e.target,m=document.getElementById("yj-ask-msg");m.style.color="#0a6238";fetch(location.origin+"/api/ask",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({sku:20608,slug:"personal-data-map-dsar-audit",lang:"en",question:f.question.value,email:f.email.value,page:location.pathname})}).then(function(r){if(!r.ok)throw 0;m.textContent="Thanks \u2014 noted. Answers land on this page and, if you left an email, in your inbox.";f.question.value="";}).catch(function(){m.style.color="#a8551f";m.textContent="Could not send. Please try again in a minute.";});return false;}</script></div></section><section class="yj-related" style="max-width:920px;margin:56px auto 40px;padding:0 20px"><h2 style="font:600 21px/1.3 system-ui,sans-serif;margin:0 0 6px;color:#1b1f24">Same job, next tool</h2><p style="font:400 14.5px/1.5 system-ui,sans-serif;margin:0 0 20px;color:#5b6570">Free in the browser. The full version is a one-time licence, 7-day refund.</p><div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:14px"><a class="yj-rel-c" href="https://getreadystack.com/tools/membership-wordpress-plugin"><span class="yj-rel-t">Membership Plugin for WordPress: 20-Type Member & Data-Request Desk for Clubs,</span><span class="yj-rel-p">$60</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/webflow-restaurant-template"><span class="yj-rel-t">Webflow Restaurant Template — delivered as self-contained HTML you own: 26 pag</span><span class="yj-rel-p">$49</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/robots-txt-ai-crawler-audit"><span class="yj-rel-t">AI Crawler Rules - robots.txt Audit for AI Search</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/devicetree-config-language-pack"><span class="yj-rel-t">DeviceTree Config Audit: 23 dtc-Style Rules, 50 Snippets</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/en16931-einvoice-lint"><span class="yj-rel-t">EN 16931 e-invoice linter with 37 rules and 26 XML snippets for Peppol BIS 3.0</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/en301549-lint"><span class="yj-rel-t">EN 301 549 Lint — European Accessibility Act rules, with the clause number, in</span><span class="yj-rel-p">$29</span></a></div><style>.yj-rel-c{display:flex;flex-direction:column;gap:8px;padding:15px 16px;border:1px solid #e2e6ea;border-radius:6px;text-decoration:none;background:#fff}.yj-rel-c:hover{border-color:#b9c2cb;background:#fafbfc}.yj-rel-t{font:500 14.5px/1.42 system-ui,sans-serif;color:#1b1f24}.yj-rel-p{font:600 13px system-ui,sans-serif;color:#0a6238}</style></section>130 131 132<script data-yj="yj-view-beacon">(function(){try{var p=location.pathname;var k='yjv:'+p;if(sessionStorage.getItem(k))return;sessionStorage.setItem(k,'1');fetch('/api/ev',{method:'POST',keepalive:true,headers:{'Content-Type':'application/json'},body:JSON.stringify({path:p,lang:"en",ref:(document.referrer||''),src:(function(){try{return (new URLSearchParams(location.search).get('ref')||'').replace(/[^a-z_]/g,'').slice(0,12)}catch(e){return ''}})()})}).catch(function(){});}catch(e){}})();</script>133 134<script data-yj="yj-use-beacon">(function(){try{var o=document.getElementById('out');if(!o)return;var p=location.pathname,k='yju:'+p,sent=false;function fire(){if(sent||sessionStorage.getItem(k))return;if(!(o.textContent||'').replace(/\s+/g,'').length)return;sent=true;sessionStorage.setItem(k,'1');fetch('/api/ev',{method:'POST',keepalive:true,headers:{'Content-Type':'application/json'},body:JSON.stringify({path:p,lang:"en",t:'use',src:(function(){try{return (new URLSearchParams(location.search).get('ref')||'').replace(/[^a-z_]/g,'').slice(0,12)}catch(e){return ''}})()})}).catch(function(){});}var mo=new MutationObserver(function(){fire();if(sent)mo.disconnect();});mo.observe(o,{childList:true,characterData:true,subtree:true});}catch(e){}})();</script>135 