TT Lab
Get started
Learn Learning paths Courses

The Language of Banking

Scrub the personal data out of the logs and still follow the incident

Continue in TT Lab

Goal

You find personal information candidates in a transfer API log, filter out false positives with check digits, and build an export copy with masking and deterministic pseudonym tokens. Even so, you trace the incident customer's case with tokens alone, and prove that running it twice gives the same result and that no leaks remain.

Why it matters

Logs used for incident investigation have free-text sentences mixed in, and account numbers and card numbers are written in them as they are. You cannot hand them over as they are, but if you cover everything with asterisks, even the fact that "it failed three times on the same account" disappears. Regular expressions look only at shape, so they mistake even 16-digit settlement sequence numbers for cards. If you apply the check digit one more time, false positives drop tenfold and you do not kill the identifiers the investigation needs. If you attach an HMAC deterministic token to the covered value, the same account always becomes the same token so records can be linked, and it cannot be reversed without the key. A scrubbing pipeline is retried, so running it twice must give the same result.

Steps

  1. Create and run /root/mask/gen_logs.py to make /root/mask/app.log (at least 500 JSON Lines) and /root/mask/customers.csv (at least 30 customers). Mix in at least 30 accounts, at least 40 cards that pass the check digit, and at least 20 16-digit numbers that do not, and make the account that appears most often have at least 3 more lines than the second.
  2. Find all four kinds of candidates with /root/mask/find_candidates.py and write them to /root/mask/candidates.json (total, by_kind, items).
  3. Create /root/mask/luhn.py (exit code 0 if valid) and sort the card candidates into card_verified and not_card in /root/mask/candidates_luhn.json.
  4. Make /root/mask/app.scrubbed.log with /root/mask/scrub.py. Cover accounts, cards, phones, and emails by the rules in the notes below, and leave 16-digit numbers that are not cards as they are.
  5. Create /root/mask/keys/scrub.key, make scrub.py attach tokens (acct, card) to each line, and then rebuild /root/mask/app.scrubbed.log.
  6. Get the token of the account that appeared most often in app.log, and looking only at the export copy, write top_account_token, event_count, req_ids, first_ts, and last_ts to /root/mask/trace.json.
  7. Create /root/mask/leakscan.py, check that running scrub.py again on the export copy gives the same result, and leave the scan result in /root/mask/leak_report.json.
  8. Create /root/mask/policy.json and /root/mask/scrub_report.json. The numbers in the report are counted from the actual files, and policy_sha256 is the sha256 of the policy file.

Notes

Build a synthetic transfer log snapshot

Create and run /root/mask/gen_logs.py to make /root/mask/app.log and /root/mask/customers.csv.

In the field the data exists first, but here we make it. The accounts, cards, and contacts must be made-up values, and the card numbers must have correct check digits. Also mix in settlement sequence numbers that are 16 digits but have wrong check digits — the false positives of the next step are exactly those.

Find every candidate with regular expressions, leaving none out

Find account, card, phone, and email candidates with /root/mask/find_candidates.py and write them to /root/mask/candidates.json.

The file to read is /root/mask/app.log made in step 1. This step is all about recall. The false positives are filtered out in the next step, so put every 16-digit number in as a card candidate for now. line counts from 1, and value must be a string that actually exists on that line.

Sort cards from sequence numbers by check digit

Create /root/mask/luhn.py and sort the card candidates into card_verified and not_card in /root/mask/candidates_luhn.json.

What to sort is the card candidates in /root/mask/candidates.json made in step 2. Starting from the second digit from the right end, double every other digit, and subtract 9 if the doubled value is 10 or more. If the sum is divisible by 10, it passes. The grader directly runs luhn.py with numbers it makes itself, so you must keep the exit code contract.

Cover by the rules and leave the sequence numbers

Create /root/mask/scrub.py to make /root/mask/app.scrubbed.log. Leave 16-digit numbers that are not cards as they are.

The file to read is /root/mask/app.log and the result is /root/mask/app.scrubbed.log. Keep exactly the four substitution shapes in the notes of the instructions. The number of lines must be the same as the original, and do not touch numeric fields such as amount. The grader recomputes the expected values from the original and compares down to the last character.

Attach the same pseudonym token to the same account

Create /root/mask/keys/scrub.key, make scrub.py attach tokens to each line, and then rebuild /root/mask/app.scrubbed.log.

You adapt and use the /root/mask/scrub.py made in step 4. If you hash only the value, the possibilities are few and it is reversed by a dictionary attack. It must be an HMAC using a secret key. If you do not put the purpose in the message, an account and a card with the same digits become the same token.

Follow the incident customer's case with tokens alone

Investigate the export copy with the token of the account that appeared most often and make /root/mask/trace.json.

You look at the original only up to choosing the account. From then on you count by looking only at tokens.acct in app.scrubbed.log. If you write the account number again in the result file, everything done so far becomes meaningless.

Is it the same after two runs, and are there no remaining leaks

Create /root/mask/leakscan.py, run scrub.py again on the export copy to check that the result is the same, and then leave /root/mask/leak_report.json.

See whether already-covered values are caught again by the rules, and whether tokens that already exist are not recomputed. The checker must not catch every 16-digit number — if it counts even sequence numbers as leaks, nobody will use it.

Leave the export policy and the scrubbing report

Create /root/mask/policy.json and /root/mask/scrub_report.json. Count the numbers in the report from the actual files.

The mask value of the policy is the number of characters kept, and key_id is the first 12 digits of the sha256 of /root/mask/keys/scrub.key. The masked in the report is the count including duplicates, tokens is the number of distinct tokens, and policy_sha256 is the value measured over the whole policy file.