Skip to content

0.2.0 - 2026-10-03

Choose a tag to compare

@github-actions github-actions released this 03 Oct 20:26
3439f15

Release Notes

What piiflow sees grows: email, messaging and push SDKs, inheritance and interfaces, exceptions,
CommonJS modules, and identifiers of people. Expect more flows and findings than 0.1.1 on the same
code, mostly PF002 for identifiers and emails sent to processors not yet declared, and fewer
maybe-personal findings where a name like state or email turned out to be something else.
piiflow scan reports dispositions it could not carry over to a changed finding, as before.

Added

  • Sinks: Stripe's billing portal sessions (JavaScript and Python), and the Polar (polar.sh) SDK
    (@polar-sh/sdk, polar_sdk).
  • Identifiers (ADR 0015): user_id, customer_id, member_id and the like, and id on a
    person's record (user.id, customer["id"], User.id), are user.unique_id.pseudonymous
    sources. They count as personal data for PF002 to PF006; an identifier alone reaching a log is
    reported as a flow but is not a PF001 finding. On Polar, 252 new PF002 findings (identifiers to
    undeclared processors).
  • Exceptions (ADR 0014): a value thrown reaches the catch/except parameter, across calls and
    out of callbacks handed to libraries, and the error of a library call inside a try may carry
    the call's arguments (hop kind error, reported at info for review). Hop kinds throw and
    error are new in the flow facts.
  • Registries: PROVIDERS.get(kind) on a dictionary literal is any of its entries (as
    PROVIDERS[kind] already was), and PROVIDERS.get("smtp") that entry, so a provider class
    picked from a table resolves. On CTFd, every email to its SMTP and Mailgun providers.
  • Email, messaging and push sinks: Python smtplib, Flask-Mail, fastapi-mail, emails, Apprise,
    Resend, Mailjet, Brevo, Mailchimp Transactional, Vonage, python-telegram-bot, Amazon SNS,
    Firebase Cloud Messaging, Expo and Web Push; JavaScript @nestjs-modules/mailer, Wasp's
    emailSender, @tryghost/nodemailer, Amazon SNS, MailerSend, Mailchimp Transactional, Brevo,
    Mailjet, Vonage, Telegram (node-telegram-bot-api, Telegraf), Firebase Cloud Messaging, Expo,
    Web Push, Novu, Knock, Customer.io and Loops. Fixtures py/messaging and ts/messaging.
  • Sinks take receiver: true when the object a method is called on is sent with it, and the
    message builders of the standard library's email package, Flask-Mail, emails and Apprise
    put their arguments into the message.
  • Inheritance (ADR 0011): methods, constructors and fields inherited from local base classes,
    super.m() and super().m(), and members of external base classes
    (class Analytics extends PostHog) resolve. A call on a value typed with a base class (a
    parameter, a field, a property) reaches every subclass's override; a call on this follows the
    class's own method and is a dynamic_call gap where subclasses override it. Reading a Python
    property or a JavaScript getter gives what it returns. On Polar, dynamic_call gaps go from
    227 to 64; on Healthchecks, coverage gaps from 58 to 28.
  • Template methods (ADR 0013): a base class method that calls, on this, a method subclasses
    override is analysed per subclass, so this.deliver() in Notifier.notify reaches each
    subclass's own deliver without one subclass's data reaching another's. These calls are no
    longer dynamic_call gaps.
  • TypeScript interfaces (ADR 0012): a call on a value typed with an interface reaches the local
    classes that implement it, directly, through an extending interface or through a base class.
    Interfaces are open: an external module the value may also come from stays a gap.
  • Fuzzing: cargo-fuzz targets for lowering, the whole pipeline (with a determinism check),
    configuration and data maps, run by ClusterFuzzLite on pull requests and weekly.

Changed

  • state and province are classified as an address's state only in an address context: on an
    object named like an address (billing_address.state), on a type or class with other address
    fields, or beside address parameters (catalogue/classification.yml). Elsewhere they are
    application state and not sources. On PrivateGPT, a chat engine's context.state made about
    half of all flows.
  • Objects passed into calls keep their fields apart inside the callee, per call site:
    show({ owner: { id, email } }) read as input.owner.id no longer reports the email address
    (ADR 0010).
  • Objects returned from calls keep their fields apart in the caller, per call site: a helper's
    return { ...record, user: { id, email } } read as r.user.id no longer reports the email
    address. Spreads into a literal are its rest, not its identity. On documenso, medium PF001
    findings go from 23 to 14 (ADR 0009).
  • Plain objects are allocation sites: within a function, and through module-level objects and
    closures, nested fields stay apart (o = { a: { email, id } }; log(o.a.id) is no longer
    reported), and a field written through one variable is seen through another that holds the
    same object (ADR 0007).

Fixed

  • require('./m') of a CommonJS module is what the module assigned to module.exports (a class,
    an object of exports), not only the module: new (require('./mailer'))() and
    const { GhostMailer } = require('../mail') resolve. On Ghost, whose server is CommonJS,
    unresolved-method gaps go from 396 to 156 and flows from 115 to 874; time 1.6 s to 4.0 s.
  • A call on a service client (Stripe, AWS, Slack …) that is not a sink no longer returns the
    client's own configuration: stripe.subscriptions.retrieve(id) does not carry the secret key
    the client was built with (new propagator flow args_only).
  • A value named like a personal attribute that is used as a record or a class (its named fields
    read, methods other than a string's called on it, here or in a function it is passed to) is
    not that attribute: a parameter or a field read named email that holds Ghost's Email model
    is not a source by its name. On PrivateGPT, 233 maybe-personal findings go; on Polar, 118.
  • A flow of unknown category was dropped when a classified field read lay on its way, whatever
    its source; this "narrowing" now applies to request input only, as documented, so a
    maybe-personal field logged beside a classified one is no longer hidden.
  • A call whose callee has two API paths that match the same sink (an aliased import and its
    original name) made two sinks at one place, and every flow into it was reported twice. The same
    anchor at the same place is now one source or sink. On Polar, 4,077 duplicate flows go.
  • A Django EmailMessage (or EmailMultiAlternatives) built with personal data and sent with
    send() reported nothing: the data is in the message, and send() takes no arguments. On
    Healthchecks, 86 flows of addresses, names and phone numbers into its outgoing mail were missed.
  • Every new Map() (and Set, WeakMap, WeakSet) was one container for provenance, so a
    value cached in a Map, such as a Prisma client, also resolved as whatever any other Map held.
    Database query filters then reached query results. On documenso, findings go from 757 to 549.
    Containers in instance fields and loop variables over containers now resolve to what they hold
    (ADR 0008).
  • The catalogue's built-in containers match their own methods only, not calls on their elements.

Install privacy-flow 0.2.0

Install prebuilt binaries via Homebrew

brew install noru-tech/tap/piiflow

Download privacy-flow 0.2.0

File Platform Checksum
privacy-flow-aarch64-apple-darwin.tar.xz Apple Silicon macOS checksum
privacy-flow-x86_64-apple-darwin.tar.xz Intel macOS checksum
privacy-flow-aarch64-unknown-linux-musl.tar.xz ARM64 MUSL Linux checksum
privacy-flow-x86_64-unknown-linux-musl.tar.xz x64 MUSL Linux checksum

Verifying GitHub Artifact Attestations

The artifacts in this release have attestations generated with GitHub Artifact Attestations. These can be verified by using the GitHub CLI:

gh attestation verify <file-path of downloaded artifact> --repo noru-tech/privacy-flow

You can also download the attestation from GitHub and verify against that directly:

gh attestation verify <file-path of downloaded artifact> --bundle <file-path of downloaded attestation> --repo noru-tech/privacy-flow