DSAR Walkthrough
A Data Subject Access Request (DSAR) is the legal mechanism behind GDPR Article 15 and CCPA's Right to Know: a person asks "what data do you hold on me?" and you have to produce an answer, usually within 30–45 days. The hard part isn't the legal text; it's the actual mechanics of finding every file, mailbox, and shared drive that mentions one specific human and reporting it back.
This guide walks the full workflow end-to-end. By the end you'll have:
- A scan that covers every place the data could be
- An identity index that links every finding to the person it belongs to
- An HTML report you can hand to legal or attach to the response
The model
PII Crawler treats DSARs as a read over the scan database, not as a separate scan. The flow is:
one or more identity piicrawler HTML
regular scans → scan(s) → dsar → report
(find PII) (link PII to (filter by
names) person)
Three reasons to keep these as separate phases:
- Scans are expensive, queries are cheap. A 5 TB share takes hours; a DSAR query takes seconds. You don't want to re-scan for every request.
- Identity association is reversible. You can re-run the linker with different settings (different distance, different scope) without rescanning files.
- One scan satisfies many DSARs. Person A asks today, Person B asks next month. Both are answered from the same data.
Step 1: Scan the data
If you don't already have scan coverage of the systems in scope, run those scans first. Coverage is the most common gap on real DSARs: you remember the file server but forget the inboxes, or the production database export but forget the backups. Common targets:
- Network shares (
piicrawlerTUI / web UI → New Network Share Scan) - Email exports (
piicrawler scan ~/mailbox-takeout) - Local file trees (
piicrawler scan /srv/finance) - Database flat-files (SQLite/Access; see Supported File Types)
You don't need to know whose data is in there yet. The point of this phase is just to surface every piece of PII in scope. See Quickstart for the basics and Scan an SMB Network Share for network coverage.
A practical tip: enable detection for every PII type you might need to report on. Reports later in the workflow surface only the types that the underlying scan was configured to find. Turning a type back on after the fact requires a rescan.
Step 2: Run an identity scan
A regular scan tells you "there's an SSN in employees.csv at byte 1024." That's not yet useful for a DSAR. You want to know whose SSN. PII Identity Scan is the phase that links findings to names.
From the TUI, open the scan and press i (or PII Identity Scan in the web UI). You configure two things:
- PII types to detect. Match the types from the regular scan, plus Full Names.
- Association method. Same Line for spreadsheets and tabular exports, Same Paragraph for narrative documents, Character Distance (default 200) for everything else. You can start as many runs as you like with different settings and compare them; each one re-reads the source files.
For a full reference, see PII Identity Scan. For a DSAR specifically, the heuristic is:
| Source | Recommended method |
|---|---|
| HR / payroll spreadsheets, customer CSVs | Same Line |
| Contracts, narrative reports, letters | Same Paragraph |
| Mixed mailboxes, support tickets | Character Distance (200–400) |
The output is an identity run: every PII finding in the scan, tagged with the name (or names) it was nearest to, plus an Unassociated bucket for findings the linker couldn't resolve.
Don't skip the unassociated review on a real DSAR. Anything in there might still belong to the subject and just lacks proximity context: typically database dumps and CSV files where the column header carries the identity instead of a per-row name. Mark them up before you generate the report.
Step 3: Search for the subject
Once you have one or more identity runs, the search is a single command:
piicrawler dsar "Jane Doe"
This searches every scan in the local database for findings associated with that name and prints a confidence-tagged summary to stderr:
DSAR Search Results: "Jane Doe"
──────────────────────────────────────────────────
Findings: 17
Files: 4
Scans: 2
PII Types: address, dob, email, phone, ssn
/srv/hr/2024-onboarding/jane-doe-i9.pdf
[ High] ssn ***-**-6789
[ High] dob **/**/****
[ High] address *** Main St
...
Confidence levels
PII Crawler ranks each result. Always include the confidence column in the response you send to legal. It's the difference between "we found Jane's SSN" and "we found an SSN that might be Jane's."
| Confidence | Source | Meaning |
|---|---|---|
| High | Identity association, unambiguous (one name in scope) | The SSN is in line with a single occurrence of "Jane Doe." Treat as the subject's data. |
| Medium | Identity association, ambiguous (two or more names equally close) | The SSN is in line with both "Jane Doe" and another name. Manual review. |
| Low | No identity association: the subject's name appeared in the finding's surrounding context | A weaker signal: a piece of PII that mentions the name nearby in the text but didn't make it into a formal association. Manual review. |
Low-confidence results are the safety net for findings that escaped identity association (older scans without an identity run, names embedded in non-standard places). Don't ignore them, but expect more noise.
A scan can carry several identity runs, and the same value can come out unambiguous under one method and ambiguous under another. Each finding is reported at the best confidence any run reached, so adding a run can only sharpen an answer, never blur one you already had.
Useful flags
piicrawler dsar "Jane Doe" --json # also print structured JSON to stdout
piicrawler dsar "Jane Doe" --report jane-doe.html # write a self-contained HTML report
piicrawler dsar "Jane Doe" --assert-clean # exit 1 if anything found (CI gate)
--assert-clean is meant for the inverse use case: failing a CI build or a release pipeline if any PII is found for an internal test identity that was never supposed to land in the codebase. Pair it with a watch policy if you want continuous protection. See piicrawler dsar.
Name matching
The query is split on whitespace and each part has to appear as a whole word, in order. "Jane Doe" matches "Jane Doe", "Jane M. Doe", and "Jane Doe-Smith", but not "Jane Doerr" or "Jane" alone. Quote multi-word names. For people who go by a nickname, run a second query with the nickname.
Parts written straight against each other still match, so "Jane Doe" finds [email protected] and JaneDoe. A longer word around them does not: [email protected] and [email protected] are somebody else.
Step 4: Generate the HTML report
For the actual response you hand to legal, use --report:
piicrawler dsar "Jane Doe" --report jane-doe-2026-05-04.html
The output is a single self-contained HTML file with:
- An executive summary (total findings, files affected, scans searched and how many of them held anything, confidence breakdown)
- A Coverage of this search notice, when the search could not read every recorded scan or some of them have no identity run behind them
- A Review before sending notice counting the findings that still need a human decision
- A breakdown by PII type and severity
- A by-file listing of every finding, with the masked value, its confidence and the scan it came from
- The date range of the scans that were searched, so legal can see exactly what window the response covers, whether or not anything was found
The values in the report are masked the same way they are in stdout (***-**-6789, jo*****e@ex***le.com, etc.). Unmasked values stay in the local database. They never appear in the report. This is intentional: the report is meant to be reviewed and shared, the database is not. The surrounding text of each finding is not in the report for the same reason, since a context window quotes the value in full. Use --json when you need to read it during review. That output carries each finding's term and context in full. It is not masked: treat it like the database, not like the report.
If the review has to happen somewhere other than this machine, add --redact. Every SSN and date of birth is replaced with a value of the same shape but different digits, consistently, so the reviewing tool still sees which values repeat without reading the real ones. It covers the JSON, the stderr summary and the HTML report together. See dsar --redact.
Check the Medium and Low rows before you send it
Only a High confidence finding was tied to the subject by name. Medium and Low ones were reached because the subject's name is near the value in the file, and near is not the same as theirs. Two examples that come up constantly:
- A form the subject signed as their employer's representative. Their name is on it, the SSN on it is the employee's.
- A paragraph naming the subject that also lists somebody else's contact details.
These land in the report as Low confidence. Read those rows against the original files and drop anything that is not the subject's, otherwise the response you send discloses someone else's data. The report says so at the top, with the count.
Another person named beside the subject is not one of these cases either. Their name is their own data, not the subject's, so it is never reported as something you hold about the subject. It can still appear as a Low row, the way any value written next to the subject's name does, and it belongs in the same read-and-drop pass as the rest of them.
The neighbouring row of a spreadsheet or database export is no longer one of these cases. The context stored with a finding stops at its own record, so the customer listed above or below the subject in an export does not pull their values into the subject's results. See Results Storage.
Operational notes
- Each DSAR is a fresh query, so re-running the same command later picks up any scans you've added since. There's no caching to invalidate.
- A scan is searched once it has completed or been stopped. A scan still running, or one that stopped on an error, covers an unknown part of its tree, so it is left out rather than mixed into an answer that claims to be complete. Every scan left out is named in the summary and in the report, with the state that kept it out. Re-run those and search again before you treat a response as final.
- A scan you stopped is searched, and named as partly read. What it recorded before it stopped is the subject's data and is reported, but the files it never opened are missing from the answer, so the summary, the report and the
--assert-cleanline each name it with how far it got —hr-fileserver (stopped after 900 of 3,000 files)— and--jsoncarries the same underscans_partly_searched. This covers a scan you stopped by hand and one whose process was killed and whose rowscans prunetidied up afterwards: pruning changes a status, not what was read. Re-run those too before you treat a response as final. - A clean answer states its coverage. "Nothing found" is worth what the ground behind it is worth, so the summary, the report and the
--assert-cleanpass line all say how many scans were searched and what window they cover. A search over a database with no finished scan says so in as many words. - The local database is the source of truth. If someone deletes a scan, its findings disappear from future DSAR queries. Back up
~/.piicrawler/piicrawler.dbbefore pruning anything you might still be obligated to report on. See Results Storage → Backups. - Identity runs are scoped to a single scan. If your data spans multiple scans, run an identity scan on each one. The DSAR command searches across all of them automatically.
-
Triage carries forward. Findings you've marked false positive in a regular scan still appear in DSAR results. The DSAR command surfaces every match by default, so you don't accidentally exclude something that should be reported. Each
--jsonfinding carries itsverdict(unreviewed,false_positiveortrue_positive), so filter on that yourself if you want to drop findings you have already dismissed:piicrawler dsar "Jane Doe" --json 2>/dev/null \ | jq '[.findings[] | select(.verdict != "false_positive")]'Each finding also carries its
match_id, so a row you decide about while answering the request can be marked in place withpiicrawler findings mark --scan <scan_id> --match <match_id> --verdict fp, and the next DSAR search sees it.
Worked example
You receive a GDPR request from [email protected] on 2026-05-04. Your scan coverage already includes the HR file server, the support-team mailbox export, and a recent SQL dump of the customer database.
# 1. Confirm scans exist for everything in scope
piicrawler # press Enter to open the scan list, eyeball coverage
# 2. From the TUI, run an identity scan on each scan that doesn't have one yet
# (press 'i' in scan detail; method = "Same Line" for the SQL dump,
# "Same Paragraph" for the support mailbox)
# 3. Run the DSAR query
piicrawler dsar "Jane Doe" --report responses/jane-doe-2026-05-04.html
# 4. Review the HTML, then attach to your DSAR response email
The HTML report is the deliverable. The stderr summary is the sanity check (counts match expectations? confidence skews high? no surprise file showing up?). The local database is the audit trail: if Jane comes back six months later and asks the same question, the DB still has the snapshot of what was true on 2026-05-04 even after the source files have changed.
See also
- PII Identity Scan: full reference for identity association (methods, false positives, exporting CSVs)
piicrawler dsarCLI reference: every flag- Triaging Findings: how false positives recorded earlier interact with DSAR results
- Results Storage: schema for the underlying tables, useful for custom queries beyond what
dsarexposes - Security: what data leaves the machine (none of the DSAR query, none of the report)