Skip to content

fix: type columns by whole words, and always redact sensitive identifiers - #22

Merged
fabriziosalmi merged 1 commit into
mainfrom
fix/header-type-detection
Sep 6, 2026
Merged

fix: type columns by whole words, and always redact sensitive identifiers#22
fabriziosalmi merged 1 commit into
mainfrom
fix/header-type-detection

Conversation

@fabriziosalmi

Copy link
Copy Markdown
Owner

Closes #20 and #21.

What was wrong

Column types were chosen with headerLower.includes(keyword). id is a substring of provider, video_title, width and residence; date of candidate; lat of plate. 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_number was typed as a latitude, and fuzzGeoCoordinate returns 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_iva and credit_card matched 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:

codice_fiscale   RSSMRA80A01H501U   ->  RSSMRA80At1V501U
credit_card      4111111111111111   ->  4111111111111111      (unchanged)
credit_card      5500005555555559   ->  5500005555555558      (one digit)

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_number is now an identifier, provider is not.

Non-English headers. Italian, Spanish, French and German terms sit alongside the English ones. indirizzo, telefono, data_nascita, importo, codice fiscale and partita iva are 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_id category, 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:

Sensitive identifiers Identifiers Free text Numbers Dates
Mild redacted redacted light fuzz light fuzz ±10 days
Moderate redacted redacted redacted fuzzed ±30 days
Aggressive redacted redacted redacted redacted redacted

Mild is unchanged from today. Moderate gains free-text redaction, which is what gives it a defensible position between the other two.

fuzzCSVData is removed. It was a second copy of the anonymization logic that nothing called: the live path is processCSVDataAsync -> 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 test previously exited 1 with "no test specified". There is now a dependency-free suite, 68 checks, run with plain node:

$ npm test
68/68 checks passed

It is not vacuous: reintroducing substring matching in the rule matcher fails exactly the eight misclassification cases from the report.

script.js now guards its DOMContentLoaded registration with typeof 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:

mild        nome fuzzed, all five identifier columns REDACTED, plate_number REDACTED,
            importo 1250.50 -> 1250.77, empty cell still empty
moderate    everything above plus nome/provider/note REDACTED, importo still fuzzed
aggressive  everything REDACTED

Before this change, the same file under Moderate returned plate_number and credit_card unchanged.

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

  • No consistency map: the same input yields different outputs on different rows, so the output cannot be used as a relational fixture. Deliberate for now, and a separate decision.
  • generateRandomName, generateRandomEmail and parseCSV are unused, but were already unused before this change, so they are left for a separate cleanup.

🤖 Generated with Claude Code

…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>
Copilot AI lite review requested due to automatic review settings September 6, 2026 10:16
Comment thread README.md
* **🗂️ 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).
Comment thread script.js
};

document.addEventListener('DOMContentLoaded', () => {
// --- Header-based data type detection ---
Comment thread script.js

function tokensContainPhrase(tokens, phrase) {
for (let i = 0; i + phrase.length <= tokens.length; i++) {
let match = true;
Comment thread script.js
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; }
Comment thread script.js
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,

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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_id handling (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.

Comment thread script.js
Comment on lines +38 to +43
return String(header)
.normalize('NFD').replace(/[\u0300-\u036f]/g, '')
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
.toLowerCase()
.split(HEADER_SEPARATORS)
.filter(Boolean);
Comment thread script.js
Comment on lines 631 to 635
function anonymizeValue(value, header, config) {
if (!value) return value;

const dataType = detectDataType(value, header.toLowerCase());

Comment thread index.html
Comment on lines 69 to +71
<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>
@fabriziosalmi
fabriziosalmi merged commit e739c33 into main Sep 6, 2026
3 checks passed
@fabriziosalmi
fabriziosalmi deleted the fix/header-type-detection branch September 6, 2026 10:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Header type detection matches substrings: national identifiers are fuzzed as text, and some columns pass through unchanged

3 participants