New: Philter One, a redaction box for your office, is coming soon. Learn more →

Contact Us

Tell us about your stack and the privacy problems you're trying to solve. We typically respond within one business day.

Prefer email? support@philterd.ai

Please do not enter PII or PHI in this form. If you need to share an example, use a sanitized one.

Knowledge base How-to

Find the PII in a Folder of Files Before You Share or Migrate It

Scan a folder for PII with Phinder before you share or migrate it, and get a risk-ranked report of which files hold SSNs, email addresses, and other PII.

For Phinder

Before a file share gets copied to new storage, handed to a vendor, or opened up to a wider group, someone should know which files in it hold personal data. Phinder answers that. You point it at a folder, it reads every supported file, and it writes a report listing which kinds of PII it detected in which files, ranked by how sensitive they are. It does not change any file.

This guide walks through one scan of a folder tree, how to read the report, and how to turn the results into a starting redaction policy.

Detection is probabilistic. Phinder can miss PII and can flag text that is not PII, so treat the report as a map to check against your own data, not a clearance.

1. Build Phinder

Phinder is a Java command-line application. It needs Java 25 or later and Maven to build:

git clone https://github.com/philterd/phinder.git
cd phinder
mvn clean install

The runnable JAR is target/phinder-1.0.0-SNAPSHOT.jar. The examples below call it phinder.jar.

If the folder contains images (.png, .jpg, .jpeg) or scanned PDFs, install Tesseract OCR as well. Without it, those files are reported as not scanned rather than silently skipped. If Tesseract cannot find its language data, set TESSDATA_PREFIX to the directory that holds files such as eng.traineddata.

2. Check what it will read

Phinder picks a reader for each file by detecting its type, and it scans the file name as well as the contents. It reads:

  • plain text and Markdown (.txt, .md)
  • PDF, Word, and RTF (.pdf, .docx, .doc, .rtf)
  • Excel and CSV (.xlsx, .xls, .csv)
  • PowerPoint (.pptx, .ppt)
  • email (.eml, .msg)
  • log files (.log), read line by line
  • images (.png, .jpg, .jpeg), through OCR

Anything else, such as a ZIP archive or a database file, is listed in the report as an unsupported file type. On PDFs, OCR is decided page by page: a page that yields fewer than 10 characters of text and contains an image is sent to OCR. A scanned page that also has a short text stamp, such as a Bates number, is read only as far as that text.

3. Run the scan

Phinder writes report.html and report.json to the current directory and overwrites any report already there. Run it from a directory set aside for this scan, not from inside the share:

mkdir -p ~/scans/finance-share
cd ~/scans/finance-share
java -jar /opt/phinder/phinder.jar -i /mnt/finance-share -R > console.txt 2> errors.txt

-i takes a file or directory and can be repeated. Without -R, Phinder scans only the files directly inside the directory. With -R it walks every subdirectory. It does not follow symbolic links into other directories.

The console output prints each detected value with its type and confidence. That is useful at a terminal and a problem in a shared CI log, which is why the command above sends it to a file you can delete. The reports themselves record types and counts per file, not the values.

With no -p option, Phinder uses a built-in policy that looks for SSNs, credit card numbers, bank routing and IBAN numbers, passport and driver’s license numbers, email addresses, phone numbers, street addresses, first names and surnames, dates, ages, ZIP codes, IP and MAC addresses, URLs, VINs, and several other types. It does not include the PhEye person-name model. To change what it looks for, pass a Phileas policy with -p policy.json, the same JSON format Philter uses.

4. Rank the results

On a large share, the per-file listing gets long. Three options control it:

java -jar /opt/phinder/phinder.jar -i /mnt/finance-share -R \
  --sort-by risk --min-risk 50 --top 10
  • --sort-by orders the per-file sections by risk (the default), magnitude, density, or name.
  • --min-risk leaves out files scoring below the given Risk Score. The default is 0. The aggregate totals still cover every file scanned.
  • --top sets how many files appear in the HTML report’s “Best Candidates for Redaction Testing” table. The default is 20.

Reading the report

Open report.html first. If any file could not be scanned, the report lists it at the top with a reason, such as an unsupported type, an unreadable file, or an OCR failure. Those files are in no score or count, so they may hold PII the report does not show. --min-risk never hides them, and Phinder exits with code 1 whenever anything failed, which makes the scan easy to gate in a script.

Each file gets three scores:

  • The Risk Score measures sensitivity. It multiplies a per-type severity by the number of findings and by the detector’s confidence, and maps the total to a level from NONE through LOW, MEDIUM, and HIGH to CRITICAL.
  • The Magnitude Score measures volume: findings weighted by type, with every weight 1.0 unless you pass -w weights.json.
  • The Density Score is the Magnitude Score divided by the file’s word count.

Severities can be overridden with --severities severities.json, a JSON object of entity type to number, such as {"email-address": 8.0}.

report.json carries the same data for scripts. A per-file entry looks like this (illustrative values):

"/mnt/finance-share/payroll/2025-q3-adjustments.xlsx": {
  "riskScore": 33.25,
  "riskLevel": "MEDIUM",
  "magnitudeScore": 5.0,
  "densityScore": 0.004,
  "counts": {
    "ssn": 2,
    "email-address": 3
  }
}

To list the critical files and the ones that failed:

jq -r '.perFileDetails | to_entries[] | select(.value.riskLevel == "CRITICAL") | .key' report.json
jq -r '.failedFiles | to_entries[] | "\(.key)\t\(.value)"' report.json

The top level also has aggregateRiskScore, aggregateRiskLevel, aggregateCounts, filesScanned, filesReported, and filesFailed.

What to do next

Open the highest-ranked files and confirm what Phinder found. Then decide per file: move it out of scope, restrict access, or redact it before the share or migration goes ahead.

For redaction, Phinder can write a starter policy from the scan:

java -jar /opt/phinder/phinder.jar -i /mnt/finance-share -R --emit-policy starter-policy.json

It enables each entity type the scan found with a REDACT strategy, in the policy format Philter and Phileas load. Give the file a .phisql extension to get PhiSQL instead. Tune it (see Writing your first redaction policy) and measure it on a sample of your own documents with Philter Scope before you rely on it.

If the share keeps changing while you clean it up, the scan log can skip files whose content has not changed since the last run. It needs a MongoDB URI:

java -jar /opt/phinder/phinder.jar -i /mnt/finance-share -R \
  --mongodb "mongodb://localhost:27017" --skip-unchanged

Files that failed are not recorded as scanned, so they are retried on the next run.