sql-card-data-lint / index.html
getreadystack's picture
Upload index.html with huggingface_hub
a583705 verified
Raw History Blame Contribute Delete
47.9 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>PCI DSS SQL Card Data — check card data in sql migrations</title>
<meta name="description" content="This free browser tool finds specified cardholder and sensitive authentication data in SQL migrations and maps each finding to a PCI DSS Requirement 3 rule.">
<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/sql-card-data-lint"></head>
<body>
<div class="wrap">
<h1>PCI DSS SQL Card Data — check card data in sql migrations</h1>
<p class="lede">Finds cardholder and sensitive authentication data in your .sql migrations: CVV, track, PIN block, plaintext PAN columns and test card numbers, each mapped to a PCI DSS Requirement 3 rule. Runs entirely in your browser — nothing is uploaded.</p>
<textarea id="src" spellcheck="false">-- 0007_payments.sql
-- Generated from a prompt: "add card storage for saved payment methods"
BEGIN;
CREATE TABLE customer_cards (
id BIGSERIAL PRIMARY KEY,
customer_id BIGINT NOT NULL REFERENCES customers (id),
card_number VARCHAR(19) NOT NULL,
cvv CHAR(4),
pin_block CHAR(16),
track2_data VARCHAR(40),
exp_month SMALLINT,
exp_year SMALLINT,
created_at TIMESTAMPTZ DEFAULT now()
);
CREATE UNIQUE INDEX idx_customer_cards_pan ON customer_cards (card_number);
CREATE TABLE payment_audit_log (
id BIGSERIAL PRIMARY KEY,
customer_id BIGINT NOT NULL,
card_number VARCHAR(19),
gateway_response JSONB,
logged_at TIMESTAMPTZ DEFAULT now()
);
CREATE VIEW v_recent_charges AS
SELECT c.customer_id, c.card_number, ch.amount, ch.captured_at
FROM customer_cards c
JOIN charges ch ON ch.customer_id = c.customer_id;
INSERT INTO customer_cards (customer_id, card_number, cvv, exp_month, exp_year)
VALUES (1, '4111111111111111', '123', 12, 2029);
COMMIT;</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="next" style="margin-top:14px;padding:14px 16px;border:1px solid #e2d8cb;border-radius:10px;background:#fffdfa"><a href="https://getreadystack.com/api/buy/cl/polar_cl_HL23mfuzaP3RMTfsQGzbUYmVSiTyJBd8Czg9B1TIuJT" style="display:inline-block;padding:9px 14px;border-radius:8px;background:#1f5f4a;color:#fff;text-decoration:none;font-weight:600">Sweep every migration in the workspace and keep the report</a><div style="font-size:13px;color:#7a6e65;margin-top:8px">$29 once &middot; one licence key per person or team seat &middot; A QSA-led gap analysis of your data stores is commonly quoted as a five-figure engagement.</div></div>
<div id="paid"><strong>Free here:</strong> Check the .sql file open in your editor and name every column that stores card data, with its PCI DSS Requirement 3 rule id. <strong>In the editor:</strong> Sweep every migration in the workspace and write a dated, exportable findings report you keep — for team and commercial use — 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 SCD_RULES = [
{
"id": "sad-cvv-stored",
"sev": "error",
"req": "PCI DSS 3.3.1.2",
"kind": "line",
"all": [
"\\b(cvv2?|cvc2?|cav2?|csc|card_?verification(_?(code|value|data))?|security_?code)\\b"
],
"none": [],
"msg": "Card verification code column. Sensitive authentication data must not be stored after authorisation, even encrypted.",
"fix": "Drop the column and read the code from the gateway request in memory only."
},
{
"id": "sad-track-stored",
"sev": "error",
"req": "PCI DSS 3.3.1.1",
"kind": "line",
"all": [
"\\b(track_?[12]\\w*|track_?data|magstripe|magnetic_?stripe|full_?track|discretionary_?data|equivalent_?data)\\b"
],
"none": [],
"msg": "Full track / magnetic-stripe equivalent data. This is sensitive authentication data and may never be retained after authorisation.",
"fix": "Remove the column; keep only the token the processor returns."
},
{
"id": "sad-pin-stored",
"sev": "error",
"req": "PCI DSS 3.3.1.3",
"kind": "line",
"all": [
"\\b(pin|pin_?block|pinblock|pin_?verification|pvv|encrypted_?pin)\\b"
],
"none": [
"\\bpin(g|ned|point)\\b"
],
"msg": "PIN or PIN block column. Sensitive authentication data must not be stored after authorisation.",
"fix": "Remove the column. PIN translation belongs in the HSM, never in a table."
},
{
"id": "pan-plaintext-column",
"sev": "error",
"req": "PCI DSS 3.5.1",
"kind": "line",
"all": [
"\\b(pan|card_?number|cardnum|cc_?number|credit_?card(_?(no|number))?|primary_?account_?number|acct_?number)\\b",
"\\b(char|varchar|varchar2|nvarchar|nchar|text|string|clob|bigint|numeric|decimal|number)\\b"
],
"none": [
"(hash|token|encrypt|_enc\\b|enc_|masked|mask\\b|last_?4|last4|first_?6|fingerprint|vault|sha\\d|hmac|surrogate)"
],
"msg": "Primary account number stored in a readable column type. Stored PAN must be rendered unreadable wherever it is kept.",
"fix": "Store a network token or a keyed hash, plus first6/last4 in their own columns."
},
{
"id": "pan-searchable-key",
"sev": "error",
"req": "PCI DSS 3.5.1",
"kind": "line",
"all": [
"\\b(create\\s+(unique\\s+)?index|add\\s+(unique\\s+)?(index|key)|unique\\s*\\(|primary\\s+key\\s*\\()",
"\\b(pan|card_?number|cc_?number|credit_?card(_?(no|number))?)\\b"
],
"none": [
"(hash|token|last_?4|last4|fingerprint|masked)"
],
"msg": "Index or key built on the account number itself, which keeps the PAN readable and searchable in the index pages.",
"fix": "Index the token or the keyed hash column instead."
},
{
"id": "pan-in-log-table",
"sev": "error",
"req": "PCI DSS 3.3.1",
"kind": "table_col",
"all": [
"(log|logs|audit|history|events?|archive|trace|webhook|debug)",
"\\b(pan|card_?number|cc_?number|credit_?card(_?(no|number))?|cvv2?|cvc2?|track_?2|expiry|exp_?month|exp_?year)\\b"
],
"none": [
"(hash|token|last_?4|last4|masked)"
],
"msg": "Card data written into a log, audit or history table. Log stores are rarely in scope for key management and are copied everywhere.",
"fix": "Log the token and the last four digits only."
},
{
"id": "auth-response-blob",
"sev": "warn",
"req": "PCI DSS 3.3.2",
"kind": "line",
"all": [
"\\b(gateway_?response|auth(orisation|orization)?_?response|raw_?response|processor_?response|psp_?payload|payment_?payload|webhook_?body|request_?payload|card_?payload)\\b",
"\\b(json|jsonb|text|blob|clob|bytea|varbinary|xml|nvarchar\\s*\\(\\s*max)\\b"
],
"none": [
"(redact|encrypt|scrub|filtered)"
],
"msg": "Whole authorisation payload kept as a blob. These bodies routinely carry the PAN and the verification code, so the column inherits every rule above.",
"fix": "Persist named fields you have reviewed, or redact the payload before insert."
},
{
"id": "pan-view-unmasked",
"sev": "warn",
"req": "PCI DSS 3.4.1",
"kind": "view",
"all": [
"\\b(pan|card_?number|cc_?number|credit_?card(_?(no|number))?)\\b"
],
"none": [
"(mask|substr|substring|right\\s*\\(|left\\s*\\(|last_?4|last4|token|hash|\\*{4})"
],
"msg": "View exposes the full account number. When PAN is displayed it must be masked to at most the first six and last four digits.",
"fix": "Wrap the column: 'XXXXXX' || RIGHT(card_ref, 4) AS card_display."
},
{
"id": "live-pan-literal",
"sev": "error",
"req": "PCI DSS 6.5.5",
"kind": "luhn",
"all": [],
"none": [],
"msg": "Luhn-valid account number written straight into the migration. A card number in version control is card data in every clone, backup and CI log.",
"fix": "Seed with a processor test token, or a number that is not Luhn-valid."
},
{
"id": "no-retention-column",
"sev": "warn",
"req": "PCI DSS 3.2.1",
"kind": "table",
"all": [],
"none": [],
"msg": "Table holds card data but defines no retention or purge column, so nothing tells you when the row may be deleted.",
"fix": "Add purge_after DATE (or retain_until) and a scheduled delete that reads it."
}
]
;</script>
<script>// SQL Card Data Lint — the one brain. Runs in Node (the extension) and in the browser (the free web page).
'use strict';
(function (root) {
var RULES = (typeof module !== 'undefined' && module.exports)
? require('./rules.json')
: (root.SCD_RULES || []);
var CARD_TABLE_RE = /\b(pan|card_?number|cc_?number|credit_?card|cardholder|card_?token|card_?ref|card_?fingerprint|card_?data)\b/i;
var RETENTION_RE = /\b(purge_?after|purge_?at|retain_?until|retention|delete_?after|expires_?at|expire_?at|valid_?until|ttl)\b/i;
var CREATE_TABLE_RE = /\bcreate\s+(?:or\s+replace\s+)?(?:global\s+temporary\s+)?table\s+(?:if\s+not\s+exists\s+)?[`"\[]?([A-Za-z_][\w$.]*)[`"\]]?/i;
var ALTER_TABLE_RE = /\balter\s+table\s+[`"\[]?([A-Za-z_][\w$.]*)[`"\]]?/i;
var INSERT_RE = /\binsert\s+into\s+[`"\[]?([A-Za-z_][\w$.]*)[`"\]]?/i;
var CREATE_VIEW_RE = /\bcreate\s+(?:or\s+replace\s+)?(?:materialized\s+)?view\b/i;
var COL_RE = /^\s*[`"\[]?([A-Za-z_][\w$]*)[`"\]]?\s+/;
var DIGITS_RE = /\d[\d \-]{11,24}\d/g;
function rx(src, extra) { return new RegExp(src, 'i' + (extra || '')); }
function luhn(d) {
var sum = 0, alt = false;
for (var i = d.length - 1; i >= 0; i--) {
var n = d.charCodeAt(i) - 48;
if (alt) { n *= 2; if (n > 9) n -= 9; }
sum += n; alt = !alt;
}
return sum % 10 === 0;
}
function codeOf(line) {
var i = line.indexOf('--');
return i >= 0 ? line.slice(0, i) : line;
}
function isComment(line) {
var t = line.replace(/^\s+/, '');
return t.indexOf('--') === 0 || t.indexOf('#') === 0 || t.indexOf('/*') === 0 || t.indexOf('*') === 0;
}
function matchAll(rule, text) {
var i;
for (i = 0; i < (rule.all || []).length; i++) if (!rx(rule.all[i]).test(text)) return false;
for (i = 0; i < (rule.none || []).length; i++) if (rx(rule.none[i]).test(text)) return false;
return true;
}
function check(text, opts) {
opts = opts || {};
var lines = String(text == null ? '' : text).split(/\r?\n/);
var findings = [];
var byRule = {};
var table = null, inTable = false, inView = false;
var tableStart = 0, tableText = '', tables = 0, cardTables = 0, colCount = 0;
function push(rule, n, extra) {
byRule[rule.id] = (byRule[rule.id] || 0) + 1;
findings.push({
check: rule.id,
sev: rule.sev || 'error',
req: rule.req || '',
msg: rule.msg + (extra ? ' (' + extra + ')' : ''),
fix: rule.fix || '',
line: n
});
}
for (var n = 0; n < lines.length; n++) {
var raw = lines[n], code = codeOf(raw), lineNo = n + 1;
var mT = CREATE_TABLE_RE.exec(code);
if (mT) { table = mT[1]; inTable = true; tableStart = lineNo; tableText = ''; tables++; }
else if (ALTER_TABLE_RE.test(code)) { table = ALTER_TABLE_RE.exec(code)[1]; }
else if (INSERT_RE.test(code)) { table = INSERT_RE.exec(code)[1]; }
else if (/\bcreate\s+(?:unique\s+)?index\b/i.test(code)) {
var mi = /\bon\s+[`"\[]?([A-Za-z_][\w$.]*)/i.exec(code);
if (mi) table = mi[1];
}
if (CREATE_VIEW_RE.test(code)) { inView = true; table = null; }
if (inTable) {
tableText += ' ' + code;
if (COL_RE.test(code) && !mT) colCount++;
}
for (var r = 0; r < RULES.length; r++) {
var rule = RULES[r];
if (rule.kind === 'line') {
if (isComment(raw)) continue;
if (matchAll(rule, code)) push(rule, lineNo, table ? 'table ' + table : '');
} else if (rule.kind === 'table_col') {
if (isComment(raw) || !table) continue;
if (rx(rule.all[0]).test(table) && matchAll({ all: [rule.all[1]], none: rule.none }, code)) {
push(rule, lineNo, 'table ' + table);
}
} else if (rule.kind === 'view') {
if (isComment(raw) || !inView) continue;
if (matchAll(rule, code)) push(rule, lineNo, '');
} else if (rule.kind === 'luhn') {
var m, hits = [];
DIGITS_RE.lastIndex = 0;
while ((m = DIGITS_RE.exec(raw)) !== null) {
var d = m[0].replace(/[^0-9]/g, '');
if (d.length >= 13 && d.length <= 19 && /^[3-6]/.test(d) && luhn(d)) hits.push(d.slice(0, 6) + '…' + d.slice(-4));
}
if (hits.length) push(rule, lineNo, hits.join(', '));
}
}
if (inView && /;\s*$/.test(code)) inView = false;
if (inTable && /^\s*\)/.test(code)) {
inTable = false;
if (CARD_TABLE_RE.test(tableText)) {
cardTables++;
if (!RETENTION_RE.test(tableText)) {
for (var q = 0; q < RULES.length; q++) {
if (RULES[q].kind === 'table') push(RULES[q], tableStart, 'table ' + table);
}
}
}
tableText = '';
}
}
var errors = 0, warnings = 0;
for (var f = 0; f < findings.length; f++) {
if (findings[f].sev === 'error') errors++; else warnings++;
}
return {
findings: findings,
summary: {
today: opts.today || '',
lines: lines.length,
rules: RULES.length,
rules_hit: Object.keys(byRule).length,
tables: tables,
card_tables: cardTables,
columns: colCount,
errors: errors,
warnings: warnings
}
};
}
var API = { engine: { check: check }, RULES: RULES, RULE_COUNT: RULES.length };
root.SCDENGINE = API;
if (typeof module !== 'undefined' && module.exports) module.exports = API;
})(typeof window !== 'undefined' ? window : globalThis);
</script>
<script>// ⛔뼈대 — kit/scaffold.py 가 찍는다 (s143). findings [{check,sev,msg,line?}] → 글/HTML. 같은 파일이 Node 와 브라우저에서 산다.
'use strict';
function escapeHtml(s) { return String(s == null ? '' : s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;'); }
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 SCDREPORT = { escapeHtml: escapeHtml, summaryLine: summaryLine, toText: toText, toHtml: toHtml };
if (typeof module !== 'undefined' && module.exports) module.exports = SCDREPORT;
else window.SCDREPORT = SCDREPORT;
</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.SCDENGINE.engine.check(src.value,{today:today.value}); out.innerHTML=window.SCDREPORT.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.sql-card-data-lint" rel="noopener" data-yj-install-door="mp" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'sql-card-data-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/sql-card-data-lint" rel="noopener" data-yj-install-door="ovsx" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'sql-card-data-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/sql-card-data-lint" rel="noopener" data-yj-install-door="npm" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'sql-card-data-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/sql-card-data-lint" rel="noopener" data-yj-install-door="mcp" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'sql-card-data-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/sql-card-data-lint" rel="noopener" data-yj-install-door="docker" onclick="try{navigator.sendBeacon('/api/ev',new Blob([JSON.stringify({t:'install',slug:'sql-card-data-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.sql-card-data-lint
npx @readystack/sql-card-data-lint &lt;file&gt;
npx @readystack/sql-card-data-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 10 rules SQL Card Data Lint (PCI DSS Req 3) 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:"sql-card-data-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">Names every column, index, view and seed row in a .sql migration that stores card data, with its PCI DSS Requirement 3 rule id</p><p style="margin:0 0 1rem;color:#5b6b7c;font-size:.94rem">A QSA-led gap analysis of your data stores is commonly quoted as a five-figure engagement; the same migrations are read here in the editor.</p><a href="https://getreadystack.com/api/buy/cl/polar_cl_HL23mfuzaP3RMTfsQGzbUYmVSiTyJBd8Czg9B1TIuJT" 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 {"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[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-17">Updated 2026-09-17</time> · ReadyStack</p><script type="application/ld+json">{"@context":"https://schema.org","@type":"WebPage","dateModified":"2026-09-17","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/sku55916_result_card.jpg" alt="SQL Card Data Lint (PCI DSS Req 3)" loading="lazy" style="max-width:100%;border-radius:6px"></p>
<p>6 columns a QSA will fail you on sit in this 35-line migration — for backend engineers on a Rails, Django, Laravel, .NET or Node payments repository, anywhere PCI DSS applies. Here is the whole result on the sample migration shipped with the tool, <code>_fixtures/dirty.sql</code>:</p>
<p>_fixtures/dirty.sql — 13 findings, 10 rules, 9 errors, 4 warnings, 2 tables</p>
<p>L5 warn no-retention-column 3.2.1 customer_cards has no purge column L8 error pan-plaintext-column 3.5.1 card_number VARCHAR(19) L9 error sad-cvv-stored 3.3.1.2 cvv CHAR(4) L10 error sad-pin-stored 3.3.1.3 pin_block CHAR(16) L11 error sad-track-stored 3.3.1.1 track2_data VARCHAR(40) L17 error pan-searchable-key 3.5.1 UNIQUE INDEX ON (card_number) L19 warn no-retention-column 3.2.1 payment_audit_log has no purge column L22 error pan-plaintext-column 3.5.1 card_number VARCHAR(19) L22 error pan-in-log-table 3.3.1 card data written into a log table L23 warn auth-response-blob 3.3.2 gateway_response JSONB L28 warn pan-view-unmasked 3.4.1 view selects card_number unmasked L32 error sad-cvv-stored 3.3.1.2 cvv in the seed row column list L33 error live-pan-literal 6.5.5 Luhn-valid literal 411111...1111</p>
<p>The migration itself is unremarkable. The prompt was &quot;add card storage for saved payment methods&quot;; what came back was clean, well indented, and named <code>card_number</code>, <code>cvv</code>, <code>pin_block</code> and <code>track2_data</code> in <code>customer_cards</code>.</p>
<p>That is exactly the problem. Three of those columns — <code>cvv</code> at L9, <code>pin_block</code> at L10, <code>track2_data</code> at L11 — are sensitive authentication data, which PCI DSS Requirement 3.3.1 says may not be retained after authorisation at all: not hashed, not encrypted, not &quot;temporarily&quot;. <code>card_number VARCHAR(19)</code> is the account number in a readable column, which Requirement 3.5.1 does not allow.</p>
<p>The file keeps going. The index at L17 keeps the account number readable inside the index pages. <code>payment_audit_log</code> copies the same column into a log store and parks the whole gateway response in a <code>JSONB</code> column, where the verification code arrives whether you wanted it or not. The seed row at L33 puts a Luhn-valid card number into version control.</p>
<p>Here are the 6 lines from that result and what replaces each of them:</p>
<p>card_number VARCHAR(19) -&gt; card_token VARCHAR(64) cvv CHAR(4) -&gt; column deleted (3.3.1.2) pin_block CHAR(16) -&gt; column deleted (3.3.1.3) track2_data VARCHAR(40) -&gt; column deleted (3.3.1.1) UNIQUE INDEX ON (card_number) -&gt; UNIQUE INDEX ON (card_token) VALUES (&#x27;411111...1111&#x27;) -&gt; VALUES (&#x27;tok_test_9f2c41ab&#x27;)</p>
<p>Rewrite the schema around <code>card_token</code>, <code>card_last4</code> and a <code>purge_after</code> date and the linter returns zero findings: a compliant schema is quiet.</p>
<p>Why the linter you already run misses this: sqlfluff, sqlint and your database&#x27;s own checks read syntax and style, and have no view on what a column means. A chat assistant answers well about the snippet you paste, but it does not read the other migrations in the repository and does not answer the same way twice. This runs offline on the file in your editor.</p>
<p>The PCI DSS v4 requirements that were future-dated became mandatory on 31 March 2025, so Requirement 3 is assessed in full at your next annual assessment — against the schema you deployed, not the intent described in the pull request. A QSA-led gap analysis of your data stores is commonly quoted as a five-figure engagement.</p>
<p>The free web page and the free editor command check the file in front of you against all 10 rules, with the rule id and a replacement for every finding, and card numbers are always reported masked. The full version sweeps every migration in the workspace at once and writes a dated report you keep, for a change ticket or an assessor.</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/sku55916_result_card.jpg" style="width:100%;max-width:360px;border-radius:10px;display:block;margin:0 auto"><source src="/media/sku55916_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 SQL Card Data 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 .sql migration and reports every column, index, view and seed row that stores cardholder data or sensitive authentication data. Each of the 10 rules carries a PCI DSS Requirement 3 id and a replacement line. The 35-line sample migration returns 13 findings from 10 rules, with card numbers always masked.</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">Backend and platform engineers on a payments repository in Rails, Django, Laravel, .NET or Node, whose company sits inside a PCI DSS assessment scope, and the person who must answer where card data is stored on the SAQ. It is for teams merging migrations written with an assistant.</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 will a normal SQL linter not catch this?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">sqlfluff, sqlint and database checks read syntax and style. None of them know that cvv CHAR(4) is sensitive authentication data that may never be stored after authorisation, or that a unique index on card_number keeps the account number readable inside the index pages. This tool checks meaning, not formatting.</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 key?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">Free and keyless: the .sql file open in your editor, all 10 rules, rule ids and suggested replacements, forever. The full version sweeps every migration in the workspace at once and writes a dated findings report you keep for an assessor or a change ticket, and covers team and commercial use.</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 done by a person?</summary><p style="margin:8px 0 2px;font:400 14.5px/1.6 system-ui,sans-serif;color:#33404b">A QSA-led gap analysis of your data stores is commonly quoted as a five-figure engagement, and it happens once a year. The PCI DSS v4 requirements that were future-dated became mandatory on 31 March 2025, so Requirement 3 is assessed in full at your next annual assessment. This is $29 once.</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. SQL Card Data Lint (PCI DSS Req 3) checks the file you open against 10 rules from a rule set dated 2026-09-20, and points at the exact line with the fix. 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 SQL Card Data Lint actually do?", "acceptedAnswer": {"@type": "Answer", "text": "It reads a .sql migration and reports every column, index, view and seed row that stores cardholder data or sensitive authentication data. Each of the 10 rules carries a PCI DSS Requirement 3 id and a replacement line. The 35-line sample migration returns 13 findings from 10 rules, with card numbers always masked."}}, {"@type": "Question", "name": "Who is it for?", "acceptedAnswer": {"@type": "Answer", "text": "Backend and platform engineers on a payments repository in Rails, Django, Laravel, .NET or Node, whose company sits inside a PCI DSS assessment scope, and the person who must answer where card data is stored on the SAQ. It is for teams merging migrations written with an assistant."}}, {"@type": "Question", "name": "Why will a normal SQL linter not catch this?", "acceptedAnswer": {"@type": "Answer", "text": "sqlfluff, sqlint and database checks read syntax and style. None of them know that cvv CHAR(4) is sensitive authentication data that may never be stored after authorisation, or that a unique index on card_number keeps the account number readable inside the index pages. This tool checks meaning, not formatting."}}, {"@type": "Question", "name": "What is free and what needs a licence key?", "acceptedAnswer": {"@type": "Answer", "text": "Free and keyless: the .sql file open in your editor, all 10 rules, rule ids and suggested replacements, forever. The full version sweeps every migration in the workspace at once and writes a dated findings report you keep for an assessor or a change ticket, and covers team and commercial use."}}, {"@type": "Question", "name": "What would this cost done by a person?", "acceptedAnswer": {"@type": "Answer", "text": "A QSA-led gap analysis of your data stores is commonly quoted as a five-figure engagement, and it happens once a year. The PCI DSS v4 requirements that were future-dated became mandatory on 31 March 2025, so Requirement 3 is assessed in full at your next annual assessment. This is $29 once."}}, {"@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. SQL Card Data Lint (PCI DSS Req 3) checks the file you open against 10 rules from a rule set dated 2026-09-20, and points at the exact line with the fix. 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:55916,slug:"sql-card-data-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/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/pci-payment-page-script-audit"><span class="yj-rel-t">PCI Payment Page Script Audit: requirement 6.4.3 and 11.6.1 on your checkout m</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/pg-migration-lock-lint"><span class="yj-rel-t">Postgres Migration Lock Lint</span><span class="yj-rel-p">$29</span></a><a class="yj-rel-c" href="https://getreadystack.com/tools/dotnet-eol-support-gate"><span class="yj-rel-t">.NET EOL &amp; CRA Support Window Gate</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></body>
</html>