PiiNet 0.1.2

There is a newer version of this package available.
See the version list below for details.
dotnet add package PiiNet --version 0.1.2
                    
NuGet\Install-Package PiiNet -Version 0.1.2
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="PiiNet" Version="0.1.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="PiiNet" Version="0.1.2" />
                    
Directory.Packages.props
<PackageReference Include="PiiNet" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add PiiNet --version 0.1.2
                    
#r "nuget: PiiNet, 0.1.2"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package PiiNet@0.1.2
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=PiiNet&version=0.1.2
                    
Install as a Cake Addin
#tool nuget:?package=PiiNet&version=0.1.2
                    
Install as a Cake Tool

Pii.NET

Deterministic PII detection and redaction for text leaving your perimeter: LLM prompts, logs, support tickets, exports. Zero external dependencies.

using PiiNet;

var safe = Pii.Redact(userMessage).Text;   // then send it to any model

Every team wiring user text into a third-party LLM has the same problem: the prompt is a support ticket, and the support ticket contains a card number, a bank verification number, and a session token. Under GDPR, Nigeria's NDPA, or PCI DSS, that content should never have crossed the wire. Python teams reach for presidio-analyzer (6.3M downloads a month); it is Python-only, and the managed .NET answer is a paid cloud service. Microsoft.Extensions.Compliance.Redaction redacts values you already classified with attributes; it cannot find a card number leaked into free text. Pii.NET does, offline and deterministically, before the wire.

Honest scope

Pii.NET is deterministic: it finds PII that has structure and checksums, and it validates instead of pattern-matching alone. A 16-digit number that fails the Luhn checksum is not reported as a card. That is the moat over regex-only tools, and also the boundary: names, addresses, and free-form PII are out of scope in this version. If you need NER, use Presidio or an ML service; this is the zero-dependency 80% that runs in-process on every prompt and log line.

Install

dotnet add package PiiNet

Recognizers

Recognizer Name Validation Confidence
Email addresses email RFC-pragmatic structure High
Phone numbers phone E.164 or formatted, 8-15 digits, digit-run guards, date/SSN/time shapes rejected High (+), Medium (formatted)
Payment cards card Luhn checksum, 13-19 digits, groups joined by dashes or short whitespace bridges (double spaces, tabs, line wraps), network identification: Visa, Mastercard, Amex, Verve, Discover, JCB High (known network), Medium
IBAN iban ISO 13616 mod-97 checksum plus exact per-country length; case-normalized, so lowercase IBANs match High
Nigerian NIN nin 11 digits, context-gated (nin, national id, ...) Medium
Nigerian BVN bvn 11 digits, context-gated (bvn, bank verification) Medium
US SSN ssn AAA-GG-SSSS with never-issued ranges rejected (000, 666, 900+, group 00, serial 0000) Medium
IP addresses ip v4 with octet range validation; v6 verified by the framework parser High (v4 near ip/host/server context), Medium (bare v4, v6)
MAC addresses mac Six hex pairs, uniform separator, boundary guards High
Secrets secret JWT (header base64url-decodes to a JSON object with alg), API keys with named provenance (sk-, AKIA, ghp_, xoxb-), bearer tokens High, Medium (bearer)
Dates of birth dob Date shapes gated on context (dob, born, birth, ...); a date alone is not PII Medium

The Nigerian coverage is deliberate: NIN, BVN, and the Verve card network (prefix blocks 506099-506198, 507865-507964, 650002-650027, checked before Discover's 65 range) ship in the box. NIN and BVN have no checksum, so they are context-gated: eleven digits only match near a keyword, which keeps invoice and reference numbers out of your redactions.

Scanning

using PiiNet;

var matches = Pii.Scan("card 4111 1111 1111 1111, contact chidi@example.com");
foreach (var match in matches)
{
    Console.WriteLine($"{match.Type} [{match.Start}..{match.End}) {match.Confidence} {match.Metadata}");
}
// PaymentCard [5..24) High visa
// Email [34..51) High

PiiMatch carries Type, Start, Length, Value, Confidence (High/Medium), and type-specific Metadata: the card network, the IBAN country, the IP version, the secret provenance, or the phone form.

Compose a subset, or your own recognizer:

using PiiNet;

var scanner = new PiiScanner(RecognizerNames.Card, RecognizerNames.Iban);
var matches = scanner.Scan("pay GB82WEST12345698765432 or 5060 9900 0000 0008");
Console.WriteLine(string.Join(", ", matches.Select(m => $"{m.Type}:{m.Metadata}")));
// Iban:GB, PaymentCard:verve

Overlaps are resolved deterministically: the longest match wins, ties go to the leftmost, then to the higher confidence, then to the earlier-registered recognizer. An email whose local part contains a phone-shaped digit run is one email, not an email plus a phone.

Fail-loud configuration: an unknown recognizer name throws, and an empty recognizer set throws. A redaction library that silently scans for nothing is the worst possible bug.

Redaction modes

Mask (default)

Type-aware, length-preserving, PCI-style for cards (first six and last four kept per PCI DSS 3.4, configurable to full):

using PiiNet;

Console.WriteLine(Pii.Redact("card 4111 1111 1111 1111, ssn 123-45-6789, call +234 703 205 0662").Text);
// card 4111 11** **** 1111, ssn ***-**-6789, call +234 *** *** **62
Type Example output Policy
Card 4111 11** **** 1111 first6/last4 (or full with MaskCardKeepFirstSixLastFour = false)
Email ********@*********** everything but @ (domain kept with MaskEmailKeepDomain = true)
Phone +234 *** *** **62 country code and last two digits
SSN ***-**-6789 last four
IBAN GB****************5432 country code and last four
NIN/BVN/Secret *********** everything
IP/MAC/DOB ***.***.**.** separators kept

Placeholder

Stable typed tokens. The shapes are a documented contract: <EMAIL>, <PHONE>, <CARD:network:last4>, <IBAN:CC>, <NIN>, <BVN>, <SSN>, <IP:v4|v6>, <MAC>, <SECRET:provenance>, <DOB>. A Luhn-valid card outside the known network tables uses the sentinel network unknown, as in <CARD:unknown:9995>.

using PiiNet;

var options = new RedactionOptions { Mode = RedactionMode.Placeholder };
Console.WriteLine(PiiRedactor.Redact("Verve 5060 9900 0000 0008 and token sk-AbCdEf1234567890GhIjKl", options).Text);
// Verve <CARD:verve:0008> and token <SECRET:openai-api-key>

Hash

Correlation-preserving pseudonymization: lowercase SHA-256 hex of the value, with an optional salt and truncation. The same value always produces the same token, so joins and analytics survive.

using PiiNet;

var options = new RedactionOptions { Mode = RedactionMode.Hash, HashSalt = "tenant-42", HashLength = 16 };
Console.WriteLine(PiiRedactor.Redact("chidi@example.com wrote to chidi@example.com", options).Text);
// both addresses become the same 16-hex-char token

Supply a salt for anything security-relevant: unsalted hashes of guessable values (phone numbers, emails) can be reversed by hashing guesses.

Vault (reversible)

Replace with tokens, keep the originals, restore later:

using PiiNet;

var result = PiiRedactor.Redact("mail chidi@example.com about card 4111111111111111",
    new RedactionOptions { Mode = RedactionMode.Vault });
Console.WriteLine(result.Text);                        // mail <EMAIL:1:a3f2b9> about card <CARD:2:a3f2b9> (nonce varies per call)
Console.WriteLine(result.Vault!.Restore(result.Text)); // the original text, exactly

Vault tokens have the shape <TYPE:n:nonce>: n is the match ordinal and the nonce is six lowercase hex characters generated once per redaction call from a cryptographic source (RandomNumberGenerator). The nonce exists because input text can already contain token-shaped strings like <CARD:1>, accidentally or planted by an attacker who wants restoration to write PII where it never was; Restore replaces exact nonce-bearing tokens only, so such literals pass through untouched. The consequence is that vault tokens, unlike everything else in this library, are not deterministic across calls.

Send the redacted text to the model, then vault.Restore(modelResponse) re-inserts the originals into whatever the model echoed back. Security caveat: the vault is in-memory only and holds the original PII in clear text. Whoever holds the vault can undo the redaction. Persistence, encryption, and retention are deliberately yours.

API surface

Call Behavior
Pii.Scan(text) / Pii.Redact(text) Safe defaults: all recognizers, mask mode
new PiiScanner(names...) / .Scan(text) Configurable subset; unknown or empty set throws
PiiRedactor.Redact(text, options) Full control: mode, recognizers, mask/hash options
RedactionOptions.Scanner A fully configured PiiScanner (including custom IPiiRecognizer implementations) that then drives every redaction mode; mutually exclusive with Recognizers
Pii.TryScan / Pii.TryRedact / PiiScanner.TryScan / PiiRedactor.TryRedact Never throw, fuzz-tested; null in, false out

Null arguments to non-Try methods throw ArgumentNullException; configuration errors (unknown recognizer name, empty recognizer set, Scanner and Recognizers both set) throw ArgumentException immediately, never degrade to a no-op scan.

Compliance framing

Pii.NET is an engineering control, not a compliance certification. It helps you implement data minimization before text reaches a third party (GDPR art. 5(1)(c) and the NDPA's equivalent minimization duty) and PCI DSS 3.4-style PAN masking in logs and tickets. Whether a given redaction satisfies a given obligation is a question for your DPO, not for a NuGet package.

Limitations

Read this before you ship.

  • No names, no addresses, no NER. Deterministic structure and checksums only. "My name is Chidi Okafor and I live at 14 Marina Road" passes through untouched.
  • Phone detection trades recall for precision. Bare unformatted digit runs (07032050662) are not matched, because they are indistinguishable from invoice numbers; a number only matches with a leading + or internal formatting, and date shapes, SSN shapes, and candidates directly followed by a :mm time are rejected. Formatted runs that are not phone numbers (1234-5678) can still match. On business documents dense with reference numbers, consider acting only on High confidence phone matches (the ones with a + country code).
  • The Luhn checksum filters, it does not identify. About 10% of random digit runs of a given length pass Luhn, and EAN/GTIN barcode numbers use the same check digit algorithm, so a 13-digit EAN-13 can validate as a card-shaped number. On inventory-heavy text, filter matches to known networks (match.Metadata is not null) instead of accepting the medium-confidence unknown-network matches.
  • Two card numbers separated by a single space merge and drop. Whitespace bridging (which catches double-spaced and line-wrapped cards) means 4111111111111111 5500005555555559 reads as one 32-digit run, which is outside 13-19 digits and matches nothing. This is pinned by a test; put a non-whitespace delimiter between adjacent PANs or scan them line by line.
  • A card followed by bridged digit noise merges and drops. Because bridging is greedy, a real card followed by up to two whitespace characters and a trailing digit run (a CVV in the next table cell, a stray reference number) grows past 19 digits and matches nothing. Also pinned by a test. Put a non-digit token (CVV:, |) between adjacent digit fields, or scan tabular data line by line.
  • A bare dotted quad is structurally identical to a version number. 1.2.3.4 in "app v1.2.3.4" and a real IP address cannot be told apart by shape, so bare IPv4 is medium confidence (high needs nearby ip/host/server-style context). For CI output, dependency reports, and changelogs, either disable the ip recognizer or act only on High.
  • Context gating is a heuristic. NIN, BVN, dates of birth, and IPv4 confidence use a keyword window (40 characters). An identifier without its keyword is missed; eleven digits near the word "bvn" that are not a BVN are matched.
  • Redaction is idempotent by construction (masked output never re-triggers a recognizer), with one documented exception: in hash mode, a hex digest that lands in a live gating context (right after the word Bearer, for example) can be re-detected as a token.
  • Vault tokens are not deterministic. Every other output of this library is a pure function of its input; vault tokens embed a per-call cryptographic nonce (see the vault section) precisely so that they cannot be predicted or planted.
  • Offsets are UTF-16 code units into the original string, the same units string.Substring uses. Emoji and RTL neighborhoods are covered by tests.
  • IPv6 is pragmatic. Candidates are verified with the framework parser, so times and MAC addresses never match, but exotic textual forms may be missed.
  • The scanner is culture-independent and ASCII-focused; fullwidth digits and other exotic Unicode digit shapes are not treated as digits.

Performance

Scanning is linear in input size on both realistic and hostile corpora. That claim is scoped to what the committed tests actually assert (Release, .NET 8; each test pins a doubling ratio under 3x and an absolute ceiling, so a quadratic regression fails CI):

  • Realistic mixed document, all recognizers (LinearityTests): 10 MB in ~0.5 s with ~160,000 matches; 5 MB in ~0.23 s.
  • Hostile corpora (HostileCorpusLinearityTests): dotted digit runs (1.2.3. repeated, the email backtracking corpus) ~0.2 s for 2 MB; date-dense text (a birthdate every 15 characters) ~0.4 s for 2 MB; secret-dense text ~0.13 s for 2 MB.

The numbers are indicative for one machine; the linearity assertions are the contract.

Roadmap

  • Pii.Net.Extensions.AI: IChatClient middleware that redacts prompts on the way into any Microsoft.Extensions.AI pipeline, as a separate package
  • NER adapter interface so an ML name/address detector can plug into the same PiiScanner and redaction modes
  • Streaming spans: scan ReadOnlySpan<char> windows over large files without materializing strings
  • More national identifiers where a deterministic rule exists

Sibling packages

Sanctions.Net screens who you deal with; Pii.NET screens what you say about them.

License

MIT. See LICENSE.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net8.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.1.3 94 8/7/2026
0.1.2 90 8/4/2026