Download index.html from getreadystack/log-pii-telemetry-lint: direct link, hf CLI and curl.
- Browser
- Download file 45.8 kB
-
https://huggingface.co/spaces/getreadystack/log-pii-telemetry-lint/resolve/main/index.html
- Command line
-
hf download hf://spaces/getreadystack/log-pii-telemetry-lint/index.html
-
curl -L -o index.html https://huggingface.co/spaces/getreadystack/log-pii-telemetry-lint/resolve/main/index.html
45.8 kB
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width,initial-scale=1"> | |
| <title>Console log request body — check personal data in your logs</title> | |
| <meta name="description" content="This browser tool identifies lines where a Node or TypeScript service writes personal data into logs, crash reports, or analytics."> | |
| <style> | |
| :root{color-scheme:light}body{margin:0;font:15px/1.55 system-ui,-apple-system,"Segoe UI",sans-serif;background:#faf9f5;color:#1a1512} | |
| .wrap{max-width:960px;margin:0 auto;padding:28px 18px 60px}h1{font-size:26px;margin:0 0 6px}.lede{color:#4a403a;margin:0 0 16px} | |
| textarea{width:100%;min-height:260px;font:13px/1.45 ui-monospace,Menlo,Consolas,monospace;padding:12px;border:1px solid #d9d0c4;border-radius:8px;box-sizing:border-box} | |
| .bar{display:flex;gap:10px;align-items:center;margin:12px 0}button{font:600 15px system-ui;padding:10px 18px;border:0;border-radius:8px;background:#1a1512;color:#fff;cursor:pointer} | |
| input[type=date]{font:14px system-ui;padding:8px;border:1px solid #d9d0c4;border-radius:6px} | |
| table{border-collapse:collapse;width:100%;font-size:14px;margin-top:10px}th,td{text-align:left;padding:8px 10px;border-bottom:1px solid #eee;vertical-align:top}th{font-size:12px;letter-spacing:.06em;text-transform:uppercase;color:#7a6e65} | |
| .sev.error{color:#a8551f;font-weight:700}.sev.warn,.sev.warning{color:#96691f}.ok{color:#2a6446;font-weight:600}.sum{font-weight:600} | |
| .note{font-size:13px;color:#7a6e65;margin-top:18px}#paid{margin-top:22px;padding:14px 16px;border:1px solid #e2d8cb;border-radius:10px;background:#fffdfa} | |
| </style> | |
| <script data-grow-initializer="">!(function(){window.growMe||((window.growMe=function(e){window.growMe._.push(e);}),(window.growMe._=[]));var e=document.createElement("script");(e.type="text/javascript"),(e.src="https://faves.grow.me/main.js"),(e.defer=!0),e.setAttribute("data-grow-faves-site-id","U2l0ZTphZjljMzgyMC0yMmU5LTRhYjgtYTIxNS03OTRhMGY1N2ExYzM=");var t=document.getElementsByTagName("script")[0];t.parentNode.insertBefore(e,t);})();</script> | |
| <!--yj-canon-s101--><link rel="canonical" href="https://getreadystack.com/tools/log-pii-telemetry-lint"></head> | |
| <body> | |
| <div class="wrap"> | |
| <h1>Console log request body — check personal data in your logs</h1> | |
| <p class="lede">Names every line where your Node/TypeScript service writes personal data into logs, crash reports and analytics: console.log(req.body), pino without redact, sendDefaultPii: true. 22 rules. Runs entirely in your browser — nothing is uploaded.</p> | |
| <textarea id="src" spellcheck="false">'use strict'; | |
| // checkout service — the shape a coding assistant produces when you ask it to "add logging". | |
| const express = require('express'); | |
| const morgan = require('morgan'); | |
| const pino = require('pino'); | |
| const winston = require('winston'); | |
| const util = require('util'); | |
| const Sentry = require('@sentry/node'); | |
| const { PrismaClient } = require('@prisma/client'); | |
| const logger = pino({ level: 'debug' }); | |
| Sentry.init({ dsn: process.env.SENTRY_DSN, sendDefaultPii: true }); | |
| const audit = winston.createLogger({ transports: [new winston.transports.File({ filename: 'audit.log' })] }); | |
| const db = new PrismaClient({ log: ['query', 'info'] }); | |
| process.env.DEBUG = '*'; | |
| const FALLBACK_CONTACT = 'ops@acme-health.co.uk'; | |
| const app = express(); | |
| app.use(morgan('combined')); | |
| app.use(express.json()); | |
| app.post('/signup', async (req, res) => { | |
| console.log('signup payload', req.body); | |
| console.log(req.headers); | |
| console.debug('cookies', req.cookies); | |
| console.info('hit', req.originalUrl); | |
| const user = await createUser(req.body); | |
| console.log(user); | |
| console.log('confirmed email', user.email); | |
| console.warn('temporary password', req.body.password); | |
| console.error(`welcome mail to ${user.email} failed`); | |
| console.log('from ip', req.ip); | |
| logger.info(req.body, 'new user'); | |
| audit.info(JSON.stringify(req)); | |
| audit.debug(util.inspect(user)); | |
| analytics.identify(user.id, { email: user.email, plan: user.plan }); | |
| try { | |
| await mailer.send(user.email, FALLBACK_CONTACT); | |
| } catch (err) { | |
| res.status(500).send(err.stack); | |
| } | |
| res.json({ ok: true }); | |
| }); | |
| module.exports = app;</textarea> | |
| <div class="bar"><button id="go">Check</button><label>as of <input type="date" id="today"></label></div> | |
| <div id="out" aria-live="polite"></div> | |
| <div id="paid"><strong>Free here:</strong> Names every line in the file you have open that writes personal data into a log, a crash report or an analytics call, with the article it breaches and the one-line fix.. <strong>In the editor:</strong> Sweeps every .js/.ts file in the workspace and writes the findings out as a report file you keep. — that part asks for a licence key.</div> | |
| <p class="note">Same engine as the VS Code extension, byte for byte.</p> | |
| </div> | |
| <script>var PIILOG_RULES = [ | |
| { | |
| "id": "console_request_body", | |
| "sev": "error", | |
| "re": "console\\.(log|info|debug|warn|error|dir|table|trace)\\s*\\([^;]*\\breq(uest)?\\.(body|params|query)\\b", | |
| "flags": "", | |
| "msg": "console writes the request body to stdout. A signup or checkout body is names, emails and card data — GDPR Art. 5(1)(c) data minimisation. Log req.body.orderId, not req.body." | |
| }, | |
| { | |
| "id": "console_request_headers", | |
| "sev": "error", | |
| "re": "console\\.\\w+\\s*\\([^;]*\\breq(uest)?\\.(headers|rawHeaders)\\b", | |
| "flags": "", | |
| "msg": "Request headers carry Cookie, Authorization and X-Forwarded-For. Logging them copies a live session into the log store. Pick the two headers you need by name." | |
| }, | |
| { | |
| "id": "console_whole_user_record", | |
| "sev": "error", | |
| "re": "console\\.\\w+\\s*\\(\\s*(['\"`][^'\"`]*['\"`]\\s*,\\s*)?(user|customer|patient|member|profile|account|employee)\\s*[,)]", | |
| "flags": "", | |
| "msg": "A whole person record goes to the log. Every field you never chose travels with it. Log the id and the one field the line is about." | |
| }, | |
| { | |
| "id": "console_pii_field", | |
| "sev": "error", | |
| "re": "console\\.\\w+\\s*\\([^;]*\\b(email|phone|mobile|ssn|nationalId|dateOfBirth|dob|postcode|passport|iban|cardNumber|creditCard|taxId)\\b", | |
| "flags": "", | |
| "msg": "A directly identifying field is written to the log. Logs are replicated to the log vendor, to backups and to anyone with read access on the pod. Hash it or drop it." | |
| }, | |
| { | |
| "id": "console_credential_field", | |
| "sev": "error", | |
| "re": "console\\.\\w+\\s*\\([^;]*\\b(password|passwd|token|accessToken|refreshToken|apiKey|api_key|secret|otp|sessionId|authorization)\\b", | |
| "flags": "", | |
| "msg": "A credential is written in clear text. A log line is not a debugger: this one survives rotation, shipping and the vendor's own retention. Remove the argument." | |
| }, | |
| { | |
| "id": "console_cookie_or_session", | |
| "sev": "error", | |
| "re": "console\\.\\w+\\s*\\([^;]*\\b(cookies?|session|setCookie)\\b", | |
| "flags": "", | |
| "msg": "Session material in a log line means anyone who can read the log can become that user. Log the session id's hash if you need to correlate." | |
| }, | |
| { | |
| "id": "console_request_url", | |
| "sev": "warn", | |
| "re": "console\\.\\w+\\s*\\([^;]*\\breq(uest)?\\.(originalUrl|url|path)\\b", | |
| "flags": "", | |
| "msg": "The full URL carries the query string, and query strings carry ?email= and ?reset_token=. Log req.route.path instead of the raw URL." | |
| }, | |
| { | |
| "id": "json_stringify_request", | |
| "sev": "error", | |
| "re": "JSON\\.stringify\\s*\\(\\s*(req|request|user|customer|body|payload)\\b", | |
| "flags": "", | |
| "msg": "Serialising the whole object defeats every redaction rule you configured downstream — the redactor sees one long string. Serialise the fields you picked." | |
| }, | |
| { | |
| "id": "util_inspect_personal_data", | |
| "sev": "error", | |
| "re": "(util\\.)?inspect\\s*\\(\\s*(req|request|user|customer|body)\\b", | |
| "flags": "", | |
| "msg": "util.inspect walks nested objects and prints getters too, so it reveals more than the shape you think you are logging. Name the fields." | |
| }, | |
| { | |
| "id": "template_literal_pii", | |
| "sev": "error", | |
| "re": "(console\\.\\w+|\\blog(ger)?\\.\\w+)\\s*\\(\\s*`[^`]*\\$\\{[^}]*\\.(email|phone|name|ssn|password|token|address|dob)\\b", | |
| "flags": "", | |
| "msg": "Interpolated personal data cannot be redacted by a structured logger — it is already flattened into the message string. Pass it as a separate, redactable field." | |
| }, | |
| { | |
| "id": "logger_request_object", | |
| "sev": "error", | |
| "re": "\\b(logger|log|winston|pino|bunyan|fastify\\.log|req\\.log)\\.(info|debug|warn|error|trace|fatal|child)\\s*\\([^;]*\\breq(uest)?\\.(body|headers|params|query|cookies)\\b", | |
| "flags": "", | |
| "msg": "The structured logger is handed the raw request. Redaction paths only cover what you listed; everything else ships. Pass an explicit object." | |
| }, | |
| { | |
| "id": "logger_ip_field", | |
| "sev": "warn", | |
| "re": "(console\\.\\w+|\\b(logger|log)\\.\\w+)\\s*\\([^;]*\\b(ip|ipAddress|remoteAddress|clientIp|forwardedFor)\\b", | |
| "flags": "", | |
| "msg": "An IP address is personal data in the EU (CJEU C-582/14, Breyer). If you log it, it needs a retention limit and a line in the privacy notice." | |
| }, | |
| { | |
| "id": "pino_without_redact", | |
| "sev": "warn", | |
| "re": "\\bpino\\s*\\(\\s*\\{(?![^)]*redact)[^)]*\\}", | |
| "flags": "", | |
| "msg": "pino is initialised without a redact option, so nothing is masked at the logger. Add redact: ['req.headers.authorization', 'req.headers.cookie', '*.email']." | |
| }, | |
| { | |
| "id": "winston_file_without_rotation", | |
| "sev": "warn", | |
| "re": "transports\\.File\\s*\\(\\s*\\{(?![^)]*(maxFiles|maxsize|maxSize))[^)]*\\}", | |
| "flags": "", | |
| "msg": "A file transport with no maxFiles or maxsize keeps personal data until the disk fills — GDPR Art. 5(1)(e) storage limitation has no 'forever' option. Set a limit." | |
| }, | |
| { | |
| "id": "sentry_send_default_pii", | |
| "sev": "error", | |
| "re": "sendDefaultPii\\s*:\\s*true", | |
| "flags": "", | |
| "msg": "sendDefaultPii: true tells the Sentry SDK to attach the client IP, cookies, headers and request body to every event. Turn it off, or set a beforeSend that strips them." | |
| }, | |
| { | |
| "id": "morgan_ip_log", | |
| "sev": "warn", | |
| "re": "morgan\\s*\\(\\s*['\"`](combined|common|dev)['\"`]", | |
| "flags": "", | |
| "msg": "The combined and common formats write the client IP and the full request line on every request. Use a custom format that omits :remote-addr, or shorten retention." | |
| }, | |
| { | |
| "id": "log_level_debug_committed", | |
| "sev": "warn", | |
| "re": "\\b(LOG_LEVEL|LOGLEVEL|logLevel|level)\\s*[:=]\\s*['\"](debug|trace|verbose|silly)['\"]", | |
| "flags": "", | |
| "msg": "A debug level committed to the repo is a debug level in production. Debug frames print the arguments the info level was written to hide. Read the level from the environment." | |
| }, | |
| { | |
| "id": "debug_env_wildcard", | |
| "sev": "warn", | |
| "re": "\\bDEBUG\\s*[:=]\\s*['\"]?\\*", | |
| "flags": "", | |
| "msg": "DEBUG=* turns on every library's debug namespace, including the HTTP clients that print request bodies and auth headers. Name the namespaces you want." | |
| }, | |
| { | |
| "id": "stack_trace_to_client", | |
| "sev": "error", | |
| "re": "\\.(send|json|end)\\s*\\([^;]*\\b\\w*[eE]rr\\w*\\.(stack|message)\\b", | |
| "flags": "", | |
| "msg": "A stack trace or driver message returned to the caller leaks file paths, query fragments and sometimes the row that failed. Return a code; log the detail server-side." | |
| }, | |
| { | |
| "id": "analytics_identify_pii", | |
| "sev": "error", | |
| "re": "\\b(analytics|mixpanel|amplitude|posthog|gtag|datadogLogs|segment)\\b[^;]*\\.(track|identify|capture|event|log)\\s*\\([^;]*\\b(email|phone|name|ssn|address|userEmail)\\b", | |
| "flags": "", | |
| "msg": "Personal data sent to an analytics processor is a disclosure to a third party — it needs a legal basis and a processor agreement, and it has to be deletable on request. Send a pseudonymous id." | |
| }, | |
| { | |
| "id": "orm_query_logging", | |
| "sev": "warn", | |
| "re": "\\blog\\s*:\\s*\\[[^\\]]*['\"](query|info)['\"]", | |
| "flags": "", | |
| "msg": "ORM query logging echoes every statement it runs, and WHERE clauses are built from customer identifiers. Keep it to local development." | |
| }, | |
| { | |
| "id": "real_email_literal", | |
| "sev": "warn", | |
| "re": "['\"][A-Za-z0-9._%+-]+@(?!example\\.|test\\.|localhost)(?:[A-Za-z0-9-]+\\.)+[A-Za-z]{2,}['\"]", | |
| "flags": "", | |
| "msg": "A real-looking mailbox is hardcoded in the source. Fixtures and fallbacks belong on example.com, which RFC 2606 reserves so it can never reach a person." | |
| } | |
| ];</script> | |
| <script>; | |
| // One brain, two homes: require() inside the extension, window.PIILOG_RULES inside the free web page. | |
| var RULES = (typeof module !== 'undefined' && module.exports) ? require('./rules.json') : window.PIILOG_RULES; | |
| var COMPILED = RULES.map(function (r) { | |
| var re = null; | |
| try { re = new RegExp(r.re, r.flags || ''); } catch (e) { re = null; } | |
| return { id: r.id, sev: r.sev, msg: r.msg, re: re }; | |
| }); | |
| var engine = { | |
| check: function (text, opts) { | |
| opts = opts || {}; | |
| var lines = String(text == null ? '' : text).split(/\r?\n/); | |
| var findings = []; | |
| for (var i = 0; i < lines.length; i++) { | |
| var line = lines[i]; | |
| if (!line || line.length > 4000) continue; | |
| for (var j = 0; j < COMPILED.length; j++) { | |
| var rule = COMPILED[j]; | |
| if (rule.re && rule.re.test(line)) { | |
| findings.push({ check: rule.id, sev: rule.sev, msg: rule.msg, line: i + 1 }); | |
| } | |
| } | |
| } | |
| return { findings: findings, rule_count: COMPILED.length, today: opts.today || '' }; | |
| } | |
| }; | |
| var API = { engine: engine, RULES: RULES, RULE_COUNT: RULES.length }; | |
| if (typeof window !== 'undefined') { window.PIILOGENGINE = API; } | |
| if (typeof module !== 'undefined' && module.exports) { module.exports = API; } | |
| </script> | |
| <script>// ⛔뼈대 — kit/scaffold.py 가 찍는다 (s143). findings [{check,sev,msg,line?}] → 글/HTML. 같은 파일이 Node 와 브라우저에서 산다. | |
| ; | |
| function escapeHtml(s) { return String(s == null ? '' : s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'); } | |
| function summaryLine(res) { | |
| const f = (res && res.findings) || []; const e = f.filter(function (x) { return String(x.sev || 'error').toLowerCase().startsWith('err'); }).length; | |
| return f.length ? (f.length + ' finding' + (f.length === 1 ? '' : 's') + ' (' + e + ' error' + (e === 1 ? '' : 's') + ')') : 'Clean — no findings.'; | |
| } | |
| function toText(res, label) { | |
| const f = (res && res.findings) || []; | |
| const lines = ['== ' + (label || '') + ' — ' + summaryLine(res)]; | |
| f.forEach(function (x) { lines.push(' ' + String(x.sev || 'error').toUpperCase().slice(0, 4) + ' ' + (x.line ? 'L' + x.line + ' ' : '') + (x.check || x.id || '') + ' ' + (x.msg || x.message || '')); }); | |
| return lines.join('\n'); | |
| } | |
| function toHtml(res) { | |
| const f = (res && res.findings) || []; | |
| if (!f.length) return '<p class="ok">' + escapeHtml(summaryLine(res)) + '</p>'; | |
| const rows = f.map(function (x) { return '<tr><td class="sev ' + escapeHtml(String(x.sev || 'error')) + '">' + escapeHtml(String(x.sev || 'error')) + '</td><td>' + (x.line ? 'L' + escapeHtml(x.line) : '') + '</td><td><code>' + escapeHtml(x.check || x.id || '') + '</code></td><td>' + escapeHtml(x.msg || x.message || '') + '</td></tr>'; }).join(''); | |
| return '<p class="sum">' + escapeHtml(summaryLine(res)) + '</p><table><thead><tr><th>Level</th><th>Line</th><th>Check</th><th>What</th></tr></thead><tbody>' + rows + '</tbody></table>'; | |
| } | |
| var PIILOGREPORT = { escapeHtml: escapeHtml, summaryLine: summaryLine, toText: toText, toHtml: toHtml }; | |
| if (typeof module !== 'undefined' && module.exports) module.exports = PIILOGREPORT; | |
| else window.PIILOGREPORT = PIILOGREPORT; | |
| </script> | |
| <script> | |
| (function(){ | |
| var src=document.getElementById('src'), out=document.getElementById('out'), go=document.getElementById('go'), today=document.getElementById('today'); | |
| today.value=new Date().toISOString().slice(0,10); | |
| function run(){ try { var res=window.PIILOGENGINE.engine.check(src.value,{today:today.value}); out.innerHTML=window.PIILOGREPORT.toHtml(res); } catch(e){ out.innerHTML='<p class="sev error">Could not run: '+String(e.message||e)+'</p>'; } } | |
| go.onclick=run; if (src.value.trim()) run(); | |
| })(); | |
| </script> | |
| <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.log-pii-telemetry-lint" rel="noopener" data-yj-install-door="mp" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'log-pii-telemetry-lint',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/log-pii-telemetry-lint" rel="noopener" data-yj-install-door="ovsx" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'log-pii-telemetry-lint',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/log-pii-telemetry-lint" rel="noopener" data-yj-install-door="npm" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'log-pii-telemetry-lint',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/log-pii-telemetry-lint" rel="noopener" data-yj-install-door="mcp" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'log-pii-telemetry-lint',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/log-pii-telemetry-lint" rel="noopener" data-yj-install-door="docker" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'log-pii-telemetry-lint',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.log-pii-telemetry-lint | |
| npx @readystack/log-pii-telemetry-lint <file> | |
| npx @readystack/log-pii-telemetry-lint --mcp</pre><p style="margin:.6rem 0 0;font-size:.92rem;color:#41505f"><a href="https://getreadystack.com/api/buy/cl/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> | |
| <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 the full rule table as a PDF</b><div style="opacity:.78;margin:.35rem 0 .8rem">All 22 rules PII-in-Logs Lint (console.log(req.body)) checks, each with its source and date, on printable pages you can hand to your team or auditor. We email it now, and once more only if a rule changes. 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">Email me the PDF</button></div><div id="yjao" hidden style="margin-top:.6rem;color:#0f7b3f">Sent. It arrives within the hour - check spam too.</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:"log-pii-telemetry-lint",lang:"en",source:"rule_table"+((b.closest&&b.closest("section")&&b.closest("section").getAttribute("data-yj-moved"))?"_at_result":"")})}).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><script>(function(){var e=document.getElementById("yjae"),s=e&&e.closest?e.closest("section"):null,o=document.getElementById("out");if(!s||!o)return;var d=false;function mv(ev){if(d)return;if(ev&&ev.target&&s.contains(ev.target))return;d=true;var b=o;while(b.parentNode&&b.parentNode!==document.body&&b.parentNode.tagName!=="MAIN"&&b.parentNode.tagName!=="ARTICLE")b=b.parentNode;if(!b.parentNode||b===s||b.contains(s))return;try{b.parentNode.insertBefore(s,b.nextSibling);s.setAttribute("data-yj-moved","1");}catch(_){}}document.addEventListener("input",mv,true);document.addEventListener("change",mv,true);})();</script></section> | |
| <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">22 rules that name the lines where a Node or TypeScript service writes personal data into logs, crash reports and analytics — with the article it touches and the one-line fix.</p><p style="margin:0 0 1rem;color:#5b6b7c;font-size:.94rem">GitHub Advanced Security lists at $49 per active committer per month ($30 Code Security + $19 Secret Protection) and looks for secrets, not for a customer's name travelling in a log line.</p><a href="https://getreadystack.com/api/buy/cl/polar_cl_NLG690BJU22qAXNWaQROOaDLw4Mg1DA9xifr10uIElw" 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> | |
| <!--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> | |
| <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> | |
| <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> | |
| <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-13">Updated 2026-09-13</time> · ReadyStack</p><script type="application/ld+json">{"@context":"https://schema.org","@type":"WebPage","dateModified":"2026-09-13","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/sku53832_result_card.jpg" alt="PII-in-Logs Lint (console.log(req.body))" loading="lazy" style="max-width:100%;border-radius:6px"></p> | |
| <p>Twenty-four findings in forty-five lines. That is what the fixture shipped with PII-in-Logs Lint returns for one Express signup route — the kind a Node engineer in Berlin or Dublin inherits in their first week — and fifteen of those twenty-four are errors, not style warnings.</p> | |
| <p>The file is not badly written. It is written the way a coding assistant writes it. Ask one to "add logging to this handler" and it logs the object it has, because the object it has is the request:</p> | |
| <pre><code>console.log('signup payload', req.body);</code></pre> | |
| <p>That line compiles, passes review, and runs for years. It copies every signup body — name, email, date of birth, sometimes a card — into stdout, into the log shipper, into the vendor's index, and into backups that outlive the service.</p> | |
| <h3>The clock is 72 hours, and it starts when someone notices</h3> | |
| <p>Art. 5(1)(c) says collect the minimum, Art. 32 says secure what you hold, and Art. 33 gives you seventy-two hours to notify a supervisory authority once you become aware of a personal-data breach. The clock starts the day someone opens the log viewer and sees a customer's address — which is how a line written in 2023 becomes a deadline in 2026. The next wave of US state privacy laws lands on 1 January 2027.</p> | |
| <h3>The findings that do not look like personal data</h3> | |
| <p>Some of the twenty-four never mention a person. <code>morgan('combined')</code> writes the client IP on every request, and an IP address is personal data in the EU — CJEU C-582/14, *Breyer*. <code>sendDefaultPii: true</code> tells the Sentry SDK to attach the IP, cookies, headers and request body to every event.</p> | |
| <p>Another group lives in the bootstrap file: <code>pino({ level: 'debug' })</code> with no <code>redact</code>, a <code>winston</code> file transport with no <code>maxFiles</code> — Art. 5(1)(e) storage limitation has no "forever" setting — a <code>DEBUG=*</code>, and an ORM started with <code>log: ['query']</code>. None of them appear in a diff that only touched a route handler.</p> | |
| <h3>The same route, written the other way</h3> | |
| <p>The second fixture is the same service: same route, same framework, same error handling. It logs <code>{ orderId }</code> instead of <code>req.body</code>, initialises <code>pino</code> with a <code>redact</code> list, sets <code>sendDefaultPii: false</code>, gives the file transport a <code>maxFiles</code>, and returns an error code instead of a stack. It scores <strong>0 findings against all 22 rules</strong>. None of those fixes are hard. They are just invisible until something counts them.</p> | |
| <h3>Why the tools you already have miss it</h3> | |
| <p>ESLint's <code>no-console</code> treats every console call identically, which is why teams switch it off — it cannot tell <code>req.body.orderId</code> from <code>req.body</code>. Secret scanners look for keys and tokens; a customer's name is neither. A chatbot will explain Art. 33 to you but cannot see that line 30 of <code>routes/signup.js</code> is the problem. The gap is not knowledge. It is location.</p> | |
| <h3>What the extension does</h3> | |
| <p>Twenty-two rules, plain regular expressions you can read in <code>ext/rules.json</code> before you trust them: request objects, person records and fields, credentials and sessions, logger configuration, third-party sinks, outward leaks, source hygiene. Each finding gives the line, the rule, the article and the fix.</p> | |
| <p>The file you have open is free — all 22 rules, no key, no limit. The paid tier, $29 once, runs the same rules across every <code>.js</code> and <code>.ts</code> file in the workspace and writes the findings to a report file you keep. One licence key per person or CI seat, 7-day full refund. GitHub Advanced Security lists at $49 per active committer per month and hunts secrets rather than people.</p> | |
| <p>Run the same 22 rules free, in the browser, on one file: https://getreadystack.com/tools/log-pii-telemetry-lint</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/sku53832_result_card.jpg" style="width:100%;max-width:360px;border-radius:10px;display:block;margin:0 auto"><source src="/media/sku53832_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 PII-in-Logs Lint actually do?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">It reads a JavaScript or TypeScript file and marks each line that sends personal data to a log sink: console.log(req.body), a whole user record, a password or token argument, sendDefaultPii: true, morgan('combined') writing the client IP, pino with no redact option, or a stack trace returned to the caller. Each finding names the rule and the fix.</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 it for?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Node and TypeScript backend engineers whose service handles EU personal data, and the platform engineer who has to say which services log what. It is written for the person who inherited a repo where logging was added by a coding assistant and nobody has read every console call since.</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 ESLint or a secret scanner not enough?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">ESLint's no-console rule treats every console call the same way, so teams switch it off; it cannot tell req.body.orderId from req.body. Secret scanners hunt keys and tokens, which are not personal data. Neither one knows that an IP address is personal data in the EU under CJEU C-582/14.</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 costs money?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">The file you have open is free, with all 22 rules, no key and no limit — you can clean a file with it and never buy anything. The paid tier, $29 once, runs the same rules across every .js and .ts file in the workspace and writes the findings to a report file you keep. Scope and ownership, not crippled rules.</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 would this cost otherwise?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">GitHub Advanced Security lists at $49 per active committer per month, split as $30 Code Security and $19 Secret Protection, and it looks for secrets rather than for personal data in log lines. A privacy engineer reading one service's logging calls by hand is a day of work per service, repeated each release.</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 not just ask ChatGPT or another AI chat?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">A general AI chat answers from training data with a cutoff date, cannot read your repository and names no rule version. PII-in-Logs Lint (console.log(req.body)) checks the file you open against 22 rules from a rule set dated 2026-09-20, and points at the exact line. For a filing, an audit or a client you need that dated result on your own files.</p></details><script type="application/ld+json" data-yj="yj-faq-ld">{"@context": "https://schema.org", "@type": "FAQPage", "mainEntity": [{"@type": "Question", "name": "What does PII-in-Logs Lint actually do?", "acceptedAnswer": {"@type": "Answer", "text": "It reads a JavaScript or TypeScript file and marks each line that sends personal data to a log sink: console.log(req.body), a whole user record, a password or token argument, sendDefaultPii: true, morgan('combined') writing the client IP, pino with no redact option, or a stack trace returned to the caller. Each finding names the rule and the fix."}}, {"@type": "Question", "name": "Who is it for?", "acceptedAnswer": {"@type": "Answer", "text": "Node and TypeScript backend engineers whose service handles EU personal data, and the platform engineer who has to say which services log what. It is written for the person who inherited a repo where logging was added by a coding assistant and nobody has read every console call since."}}, {"@type": "Question", "name": "Why is ESLint or a secret scanner not enough?", "acceptedAnswer": {"@type": "Answer", "text": "ESLint's no-console rule treats every console call the same way, so teams switch it off; it cannot tell req.body.orderId from req.body. Secret scanners hunt keys and tokens, which are not personal data. Neither one knows that an IP address is personal data in the EU under CJEU C-582/14."}}, {"@type": "Question", "name": "What is free and what costs money?", "acceptedAnswer": {"@type": "Answer", "text": "The file you have open is free, with all 22 rules, no key and no limit — you can clean a file with it and never buy anything. The paid tier, $29 once, runs the same rules across every .js and .ts file in the workspace and writes the findings to a report file you keep. Scope and ownership, not crippled rules."}}, {"@type": "Question", "name": "What would this cost otherwise?", "acceptedAnswer": {"@type": "Answer", "text": "GitHub Advanced Security lists at $49 per active committer per month, split as $30 Code Security and $19 Secret Protection, and it looks for secrets rather than for personal data in log lines. A privacy engineer reading one service's logging calls by hand is a day of work per service, repeated each release."}}, {"@type": "Question", "name": "Why not just ask ChatGPT or another AI chat?", "acceptedAnswer": {"@type": "Answer", "text": "A general AI chat answers from training data with a cutoff date, cannot read your repository and names no rule version. PII-in-Logs Lint (console.log(req.body)) checks the file you open against 22 rules from a rule set dated 2026-09-20, and points at the exact line. For a filing, an audit or a client you need that dated result on your own files."}}]}</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:53832,slug:"log-pii-telemetry-lint",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.</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/personal-data-map-dsar-audit"><span class="yj-rel-t">Personal Data Map: find every column a GDPR subject access request has to reac</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/dsa-terms-lint"><span class="yj-rel-t">DSA Terms Lint - EU Digital Services Act</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/python-pci-log-leak-lint"><span class="yj-rel-t">Python Log Leak Lint: PCI DSS Card Data</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/sql-card-data-lint"><span class="yj-rel-t">SQL Card Data Lint (PCI DSS Req 3)</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/docker-image-for-development"><span class="yj-rel-t">Docker Image for Development — Node.js 22 + TypeScript Dev Container with Post</span><span class="yj-rel-p">$19</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></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></body> | |
| </html> | |