Repository navigation
0.2.0 - 2026-10-03
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_idand the like, andidon a
person's record (user.id,customer["id"],User.id), areuser.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/exceptparameter, across calls and
out of callbacks handed to libraries, and the error of a library call inside atrymay carry
the call's arguments (hop kinderror, reported atinfofor review). Hop kindsthrowand
errorare new in the flow facts. - Registries:
PROVIDERS.get(kind)on a dictionary literal is any of its entries (as
PROVIDERS[kind]already was), andPROVIDERS.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. Fixturespy/messagingandts/messaging. - Sinks take
receiver: truewhen the object a method is called on is sent with it, and the
message builders of the standard library'semailpackage, Flask-Mail,emailsand Apprise
put their arguments into the message. - Inheritance (ADR 0011): methods, constructors and fields inherited from local base classes,
super.m()andsuper().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 onthisfollows the
class's own method and is adynamic_callgap where subclasses override it. Reading a Python
property or a JavaScript getter gives what it returns. On Polar,dynamic_callgaps 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, sothis.deliver()inNotifier.notifyreaches each
subclass's owndeliverwithout one subclass's data reaching another's. These calls are no
longerdynamic_callgaps. - 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
stateandprovinceare 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'scontext.statemade about
half of all flows.- Objects passed into calls keep their fields apart inside the callee, per call site:
show({ owner: { id, email } })read asinput.owner.idno 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 asr.user.idno 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 tomodule.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 flowargs_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 namedemailthat 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(orEmailMultiAlternatives) built with personal data and sent with
send()reported nothing: the data is in the message, andsend()takes no arguments. On
Healthchecks, 86 flows of addresses, names and phone numbers into its outgoing mail were missed. - Every
new Map()(andSet,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/piiflowDownload 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-flowYou 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