Skip to main content

Privacy Flow Inspector

CI GitHub Marketplace PyPI License: MIT

Which personal data leaves the browser, who receives it, and how?

Privacy Flow Inspector reads recorded web traffic (HAR files from Chrome, Firefox or Playwright) and reports every place where personal data is sent: in URLs, headers, cookies, JSON, forms, Base64 blobs or login tokens, including hashed forms that ad pixels use. It groups recipients into first and third parties, applies your policy and gives an APPROVE / REVIEW / REJECT decision for CI. It works offline. Reports contain no raw personal data, and it can export a redacted HAR that is safe to attach to a ticket.

Status: v0.2.0, an alpha release on PyPI and the GitHub Marketplace. Tested on Windows, Linux and macOS, including a demo that CI re-records with real Chromium on every run. Findings are technical evidence for a privacy review, not legal advice or a GDPR compliance verdict.

Part of a small software trust toolkit with Software Intake Gatekeeper: the Gatekeeper checks what is inside an app; this tool checks what the app sends out. Both use the same decision model, policy style and report formats.

Job summary for the fictional demo shop: REJECT, with a data-flow map of which data categories reach each recipient

Where it helps

Who Situation What they get
QA and end-to-end tests The Playwright suite runs on every pull request The build fails if the test user's email reaches an analytics domain
Data protection officer Before a release, a DPIA or a records-of-processing update A data-flow map of which data categories go to which recipients
Healthcare and patient portals Checking for health-data leaks Alerts when diagnosis fields or German health-insurance numbers reach third parties
Marketing / tag-manager changes A new pixel or session-replay script is added Detection of hashed emails and Base64-wrapped form snapshots

Quick start

Install from PyPI (Python 3.11+) and scan a HAR file:

python -m pip install privacy-flow-inspector

privacy-flow-inspector scan session.har --config pfi.toml `
  --json reports/report.json --sarif reports/report.sarif --markdown reports/summary.md `
  --redact-dir reports/redacted

Exit status: 0 the decision passed --fail-on (default reject), 1 the gate failed, 2 usage or input error. privacy-flow-inspector rules lists the detectors.

Recording a HAR

Tool How
Chrome / Edge DevTools Network panel, then Export HAR. The sanitized export leaves out cookies and Authorization; those then cannot be checked.
Firefox DevTools Network panel, then Save All As HAR.
Playwright (Python) browser.new_context(record_har_path="session.har", record_har_content="embed")
Playwright (Node) browser.newContext({ recordHar: { path: "session.har", content: "embed" } })

Use content="embed": with omit or attach, Playwright leaves request bodies out of the HAR. The inspector detects this and reports a blocking body-not-recorded gap instead of approving.

Try the fictional demo

examples/demo-shop is a deliberately leaky, fictional health shop. scripts/record_demo.py drives it with real Chromium and answers every request locally, so nothing leaves the machine. The recorded capture is committed as examples/recorded/demo-shop.har.

privacy-flow-inspector scan examples/recorded/demo-shop.har --config examples/pfi.toml

Real output:

Decision: REJECT
  - 6 data flow(s) at or above high.
  - 4 data flow(s) between low and high.
Requests: 10 | recipients: 7 (6 third-party) | personas: 3
Findings: critical 1, high 5, medium 2, low 2, info 4
  [CRITICAL] PFI-PERSONA          Test user email sent to legacy-ads.test via query
  [HIGH    ] PFI-PRECISE-LOCATION Precise location sent to geo-widgets.test via query
  [HIGH    ] PFI-IBAN             IBAN sent to payments-processor.test via body (allowed)
  [HIGH    ] PFI-PERSONA          Test user email sent to pixel-tracker.test via query
  [HIGH    ] PFI-PERSONA          Test user phone sent to pixel-tracker.test via query
  [HIGH    ] PFI-DE-KVNR          German health insurance number (KVNR) sent to session-cam.test via body
  [HIGH    ] PFI-HEALTH-FIELD     Health data field sent to session-cam.test via body
  [MEDIUM  ] PFI-PERSONA          Test user email sent to analytics-vendor.test via body
  ...

The Markdown summary contains a data-flow map:

Recipient Party contact identity financial health location
legacy-ads.test third CRIT
geo-widgets.test third HIGH
payments-processor.test third MED (allowed) HIGH (allowed)
pixel-tracker.test Ad pixel (fictional) third HIGH
session-cam.test Session replay (fictional) third HIGH
analytics-vendor.test Analytics vendor (fictional) third MED MED
shop.example first info info LOW

What the demo shows:

  • Hashed email and phone sent to an ad pixel (sha256(plain), sha256(digits)).
  • An email inside a nested page URL sent over plain HTTP: critical.
  • A session-replay vendor receiving a Base64-wrapped form with a health-insurance number and an ICD-10 diagnosis: health data, a GDPR Article 9 special category.
  • A payment processor marked allowed because the config lists it as a contracted processor.
  • First-party traffic still shows up (as info/low) so reviewers see the whole picture.

How it works

flowchart LR
    H[HAR files] --> P[Bounded HAR parser]
    P --> X[Value extraction: URL, query, headers, cookies, JSON, form, multipart, Base64, JWT, nested URLs]
    X --> D1[Test-persona matching incl. hashes]
    X --> D2[Checked detectors]
    D1 & D2 --> R[Recipient: eTLD+1, first/third party]
    R --> S[Severity: category, party, channel, transport]
    S --> Pol[Policy: approved processors, expiring waivers]
    Pol --> Dec{APPROVE / REVIEW / REJECT}
    Dec --> O[JSON, SARIF, Markdown, redacted HAR]

Detectors

Rule Category Evidence required
PFI-PERSONA as configured Exact test-persona value, or its lowercase, URL-encoded, Base64, MD5/SHA-1/SHA-256 or phone-digit form
PFI-EMAIL contact Email pattern (image names such as icon@2x.png excluded)
PFI-PHONE contact Whole value in E.164 format, or a phone-like number in a phone field
PFI-IBAN financial Country length and mod-97 checksum
PFI-CARD financial Known issuer prefix, matching length and Luhn checksum
PFI-DE-TAX-ID government-id Tax-ID field, ISO 7064 MOD 11,10 check digit and the official digit-repetition rules
PFI-DE-KVNR health German health insurance number with a valid check digit
PFI-HEALTH-FIELD health Non-empty value in a diagnosis, ICD, medication, symptom, allergy or similar field
PFI-IDENTITY-FIELD identity Name, birth-date or postal-address field
PFI-PRECISE-LOCATION location Latitude/longitude with four or more decimals (about 11 m)
PFI-IP-ADDRESS online-identifier Public IP in an IP field, or a whole-value IPv6 address
PFI-THIRD-PARTY-CONNECTION (opt-in) online-identifier Any request to a third party. The recipient always receives the visitor's IP address and browser details, even when the content holds no personal data (web fonts, CDN scripts)

The test-persona detector gives the most reliable results. Use a dedicated test account in your end-to-end tests, list its values in the config, and every encoded or hashed copy is found with essentially no false positives.

Every report lists all third-party recipients and their request counts, including those that received no detected personal data. To gate on those connections too, enable the opt-in rule:

[detectors]
enabled = ["PFI-THIRD-PARTY-CONNECTION"]

Approve a recipient you have a legal basis for with an [[allow]] entry for the online-identifier category.

Severity

Severity starts from the data category and then moves up or down:

Step Effect
Start health, financial, government-id: high; contact, identity, location: medium; online identifier: low
Recipient is first party two levels lower
Value in a URL, path or Referer one level higher (URLs end up in logs and proxies)
Plain HTTP one level higher

For example, an email in a JSON body to your own HTTPS API is info. The same email in a third-party URL over HTTP is critical.

Configuration

See examples/pfi.toml:

version = 1

[gate]
reject_at = "high"
review_at = "low"

[first_party]
domains = ["shop.example"]

[[persona]]
label = "Test user email"
value = "jane.test@example.com"
category = "contact"

# Contracted processors (GDPR Art. 28) may receive the listed categories.
[[allow]]
recipient = "payments-processor.test"
categories = ["financial", "contact"]
reason = "Payment processor with a signed data processing agreement."

[[waivers]]
id = "PFI-IP-ADDRESS"
recipient = "cdn.example"
reason = "CDN logs are covered by the hosting contract; reviewed by the DPO."
expires = 2026-12-31

Unknown keys are rejected. Persona values must be invented test data, never real people. Allowances and waivers need written reasons, and waivers expire. Without [first_party], the first page's site is used and the report says so.

GitHub Action

name: Privacy flows
on: [pull_request]

permissions:
  contents: read
  security-events: write   # only for the SARIF upload

jobs:
  e2e-privacy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      # ... run your Playwright tests with recordHar content "embed" into test-results/ ...
      - name: Privacy Flow Inspector
        id: privacy
        uses: Sushant-Nemade/privacy-flow-inspector@v0
        with:
          har: test-results/**/*.har
          config: .github/pfi.toml
          fail-on: reject
      - name: Upload SARIF
        if: always() && steps.privacy.outputs.sarif != ''
        uses: github/codeql-action/upload-sarif@v4
        with:
          sarif_file: ${{ steps.privacy.outputs.sarif }}
          category: privacy-flow-inspector
Input Default Description
har (required) HAR path or glob (** supported). No match is an error.
config "" TOML config (first party, personas, allowances, waivers).
fail-on reject reject, review or never.
redact true Also write redacted HAR copies to the output directory.
output-dir privacy-flow-reports Where report.json, report.sarif, summary.md and redacted/ go.

Outputs: decision and sarif. The Markdown summary is added to the job summary. The action uses hash-pinned dependencies in a private virtual environment, needs no token and makes no network requests. Do not upload raw HAR files as workflow artifacts; upload the redacted copies instead.

Privacy by design

  • Offline: no network access and no telemetry.
  • No raw values in reports: each value appears only as a short mask (ja********om) and a keyed fingerprint, so the same value can be correlated within one report without being revealed.
  • Redacted HAR export: detected values and their encoded forms are replaced, Authorization, Cookie and Set-Cookie values are removed, and response bodies are dropped. The file is re-checked and not written if anything would remain.
  • Bounded parsing of untrusted input, and escaping of all capture-derived text in Markdown.

Verification

Check How
Real browser traffic CI re-records the demo with Playwright Chromium on every run and requires exactly the expected data flows
Real website A Chromium capture of the author's live portfolio site showed APPROVE with no findings, while the page loaded Google Fonts. That gap led to the third-party recipient list and the opt-in connection rule; after the fonts were self-hosted, the same check with the rule enabled shows APPROVE with zero third parties
Leak-free outputs A test checks that no persona value, nor any of its encoded or hashed forms, appears in the JSON, SARIF, Markdown, text or redacted HAR
Checksums Steuer-ID, IBAN and Luhn compared with python-stdnum on 100,000+ random numbers. The only difference is numbers with three identical digits in a row, which the official Steuer-ID rules forbid and stdnum does not check. KVNR uses the published example A123456780.
False positives A corpus of common tracking values (timestamps, UUIDs, ETags, version numbers, private IPs, invalid checksums) must produce no findings
Action Runs on Ubuntu, Windows and macOS against the demo: decision, SARIF paths, redacted copy, fail-on and the no-match error
Code quality pytest, ruff, mypy --strict, build, twine and pip-audit on Python 3.11 and 3.13

Limitations

  • Only what the HAR contains is checked. WebSocket messages, service-worker traffic and requests made before recording started may be missing.
  • Binary or compressed payloads (protobuf, gzip in JSON) are not decoded.
  • Field-name detectors depend on meaningful field names. The test-persona detector does not.
  • Domains are grouped by registrable domain; a vendor serving from your own subdomain (CNAME cloaking) looks first-party. Name it under [labels] and review it.
  • A finding is evidence for a privacy review. Whether a flow is lawful depends on purpose, consent and contracts, which the tool cannot see.

Development

python -m pip install uv
uv sync --frozen --extra dev
uv run --frozen --extra dev pytest -q
uv run --frozen --extra dev playwright install chromium   # optional, for the recording test
$env:PFI_RECORD = "1"; uv run --frozen --extra dev pytest -q tests/test_record.py

License

MIT © 2026 Sushant Nemade. See SECURITY.md for vulnerability reporting. Recipient grouping uses publicsuffixlist (MPL-2.0) with the bundled Public Suffix List.

Metadata

Release files for privacy-flow-inspector 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for privacy-flow-inspector 0.2.0
File Size Uploaded
privacy_flow_inspector-0.2.0.tar.gz 49.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for privacy-flow-inspector 0.2.0
File Interpreter ABI Platform
privacy_flow_inspector-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 86.4 kB

Release files / privacy_flow_inspector-0.2.0.tar.gz

Download URL privacy_flow_inspector-0.2.0.tar.gz
Size 49.4 kB
Tags Source
SHA-256 checksum
How to use checksums
6aeb3629bc8d214344ba2f3025a031e6876521eafa62fcac1d13f43987b60343
BLAKE2b-256 checksum
How to use checksums
131bbe1abcb1fbb63e2358d82e775303bdf1fad9423cca38695f6138f27f354a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.

Transparency log

Release files / privacy_flow_inspector-0.2.0-py3-none-any.whl

Download URL privacy_flow_inspector-0.2.0-py3-none-any.whl
Size 37.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
615a810bf76bdaa66fed25605c4a6d003e35189557ea4adfbf96016552b81ff7
BLAKE2b-256 checksum
How to use checksums
1a697a5721566fd4bedcc2518a396d4cb7d0fa87ac99bce2ad6e2fbeba4f8f83
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

0.1.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page