fix: type columns by whole words, and always redact sensitive identifiers - #22
Conversation
…iers Column types were chosen by testing whether the header contained a keyword, so "provider", "video_title" and "width" were typed as identifiers, "candidate" as a date, and "plate_number" as a latitude. That last one is the worst case: fuzzGeoCoordinate returns its input verbatim when the value is not numeric, so a column of plates was copied into the anonymized output unchanged. Headers are now split into tokens on separators and camelCase boundaries and matched on whole tokens, with the common Italian, Spanish, French and German terms alongside the English ones, since those are exactly the columns most likely to hold personal data in a non-English dataset. Adds a sensitive_id category, always redacted at every preset and never fuzzed: codice_fiscale, iban, ssn, partita_iva and credit_card matched no rule before and were fuzzed as generic text, which returns a recognisable variant of the original. Measured on a real IBAN under Moderate, 40% of characters came back identical and in position; through the UI a card number came back with one digit changed. An IBAN is also detected by value with the mod-97 check, for columns whose header says nothing. The presets are now a ladder: Moderate redacted nothing at all, which made it weaker than Mild despite its name, and now redacts free text while leaving numbers and dates fuzzed. Removes fuzzCSVData, a second copy of the anonymization logic that nothing called: it had diverged from the live path (processCSVDataAsync -> processChunk -> anonymizeValue) and logged every original value to the browser console, one line per cell. Keeping two implementations is how this kind of defect survives. Adds a dependency-free unit suite behind `npm test`, which previously exited 1 with "no test specified". Reintroducing substring matching fails exactly the eight cases from the report. Closes #20 Closes #21 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
| * **🗂️ Structure Preserved:** Maintain the integrity of your CSV files. The anonymizer intelligently modifies data *within* the existing structure, keeping columns and formatting consistent. | ||
| * **🤩 Type-Aware Fuzzing & Redaction:** Go beyond simple string replacement. This tool understands different data types (Numbers, Dates, Emails, URLs, YouTube URLs, Geographic Coordinates, Addresses, IDs, and general Strings) and applies appropriate anonymization techniques to each. | ||
| * **🤩 Type-Aware Fuzzing & Redaction:** Go beyond simple string replacement. This tool understands different data types (Sensitive Identifiers, Numbers, Dates, Emails, URLs, YouTube URLs, Geographic Coordinates, Addresses, IDs, and general Strings) and applies appropriate anonymization techniques to each. | ||
| * **🛑 Sensitive Identifiers Are Always Redacted:** Columns detected as national identifiers, tax codes, bank accounts or card numbers are replaced with `REDACTED` at every preset, and cannot be fuzzed instead. See [How columns are typed](#-how-columns-are-typed). |
| }; | ||
|
|
||
| document.addEventListener('DOMContentLoaded', () => { | ||
| // --- Header-based data type detection --- |
|
|
||
| function tokensContainPhrase(tokens, phrase) { | ||
| for (let i = 0; i + phrase.length <= tokens.length; i++) { | ||
| let match = true; |
| for (let i = 0; i + phrase.length <= tokens.length; i++) { | ||
| let match = true; | ||
| for (let j = 0; j < phrase.length; j++) { | ||
| if (tokens[i + j] !== phrase[j]) { match = false; break; } |
| case 'moderate': | ||
| // Redacts what identifies (free text and identifiers) and fuzzes | ||
| // what is only quantitative, so the shape of the data survives | ||
| // for testing. Previously this preset redacted nothing at all, |
There was a problem hiding this comment.
🟡 Changes recommended
Header camelCase detection is currently broken in the live path due to lowercasing before tokenization, which can prevent intended redaction, and the preset help text has a ladder-direction wording error.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR hardens the CSV anonymization pipeline by fixing header-based type detection (whole-word token matching instead of substring matches), introducing an always-redacted sensitive_id category (plus IBAN value validation), and making the preset ladder monotonic in redaction strength, with accompanying documentation and a Node-based test suite.
Changes:
- Replaced substring header matching with token/phrase matching and added
sensitive_idhandling (always redacted), including IBAN mod-97 validation fallback. - Fixed preset behavior so Moderate is strictly stronger than Mild, and removed the unused/unsafe duplicate anonymization path.
- Added a dependency-free test suite and updated docs/changelog accordingly.
File summaries
| File | Description |
|---|---|
| script.js | Implements tokenized header detection, sensitive_id handling, IBAN validation, preset ladder fix, removes unused duplicate anonymizer, and exports detection utilities for Node tests. |
| test/detect-data-type.test.js | Adds unit tests covering tokenization, misclassification regressions, sensitive-id recognition, and IBAN validation. |
| README.md | Documents column typing rules and preset semantics; updates guidance around sharing outputs and corrects license/link text. |
| package.json | Updates npm test to run the new Node test file. |
| index.html | Updates preset help text to explain always-redacted sensitive identifiers and preset ladder behavior. |
| CHANGELOG.md | Records the fixes/additions/removals introduced by this PR under Unreleased. |
Review details
- Files reviewed: 6/6 changed files
- Comments generated: 3
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| return String(header) | ||
| .normalize('NFD').replace(/[\u0300-\u036f]/g, '') | ||
| .replace(/([a-z0-9])([A-Z])/g, '$1 $2') | ||
| .toLowerCase() | ||
| .split(HEADER_SEPARATORS) | ||
| .filter(Boolean); |
| function anonymizeValue(value, header, config) { | ||
| if (!value) return value; | ||
|
|
||
| const dataType = detectDataType(value, header.toLowerCase()); | ||
|
|
| <i class="fas fa-lightbulb"></i> Choose a preset for quick setup or select "Custom" for full control. | ||
| Each preset redacts everything the one above it redacts, and more. | ||
| <br> |
Closes #20 and #21.
What was wrong
Column types were chosen with
headerLower.includes(keyword).idis a substring ofprovider,video_title,widthandresidence;dateofcandidate;latofplate. So ordinary columns were typed wrongly, silently, and consistently.Two consequences made this more than a tidiness problem:
A misclassified column can pass through untouched.
plate_numberwas typed as a latitude, andfuzzGeoCoordinatereturns its input verbatim when it does not parse as a number. Plates were copied into the anonymized output unchanged.National identifiers were fuzzed as generic text.
codice_fiscale,iban,ssn,partita_ivaandcredit_cardmatched no rule at all. Fuzzing perturbs a value, so what came out was a recognisable variant of what went in. Through the UI, Mild preset:And the "Moderate" preset set every redaction flag to
false, making it weaker than "Mild", which redacts identifiers.What this changes
Whole-word matching. Headers are split into tokens on separators and camelCase boundaries, then matched on tokens and on contiguous token phrases.
plate_numberis now an identifier,provideris not.Non-English headers. Italian, Spanish, French and German terms sit alongside the English ones.
indirizzo,telefono,data_nascita,importo,codice fiscaleandpartita ivaare exactly the columns most likely to hold personal data in a non-English dataset, and all of them fell through to generic text before.A
sensitive_idcategory, always redacted. Tax codes, national identity numbers, passports, bank accounts, IBANs, card numbers and security codes. Not a preset default: a hard rule, at every preset and in Custom. There is no fuzz factor at which returning a perturbed tax code is safe, so offering one would be misleading.IBAN detection by value, with the ISO 13616 mod-97 check, for columns whose header says nothing useful. This is the only value-based sensitive detection here, deliberately: the check digits make false positives essentially impossible, whereas a Luhn check on a bare number would redact one numeric order id in ten.
The presets are a ladder. Each redacts everything the one below it redacts:
Mild is unchanged from today. Moderate gains free-text redaction, which is what gives it a defensible position between the other two.
fuzzCSVDatais removed. It was a second copy of the anonymization logic that nothing called: the live path isprocessCSVDataAsync->processChunk->anonymizeValue. It had diverged from the live path and logged every original value to the browser console, one line per cell, which is its own problem in a tool whose purpose is not to leak the input. Keeping two implementations is how this kind of defect survives.Empty cells stay empty instead of becoming
REDACTED, so the tool does not invent a value where there was none.Tests
npm testpreviously exited 1 with "no test specified". There is now a dependency-free suite, 68 checks, run with plainnode:It is not vacuous: reintroducing substring matching in the rule matcher fails exactly the eight misclassification cases from the report.
script.jsnow guards itsDOMContentLoadedregistration withtypeof document !== 'undefined'and exports the detection functions when required from Node, which is what makes them testable. Browser behaviour is unchanged.Verified end to end
Not only in unit tests. The whole app was loaded in a DOM, a CSV uploaded through the file input and the button clicked, for each preset. With the columns
nome, codice_fiscale, iban, ssn, partita_iva, credit_card, user_id, plate_number, provider, importo, note:Before this change, the same file under Moderate returned
plate_numberandcredit_cardunchanged.Also in this diff
Two factual errors in the README, small enough to fix in passing and worth naming rather than sneaking in: AGPL-3.0 was described as "permissive" (it is strong copyleft, with obligations for network use), and a
<link to your repo if public>placeholder was still in the text. The closing "you can confidently share this file" is now a prompt to check the output first, since typing is heuristic and fuzzed values are perturbations rather than replacements.Not addressed here
generateRandomName,generateRandomEmailandparseCSVare unused, but were already unused before this change, so they are left for a separate cleanup.🤖 Generated with Claude Code