PiiNet 0.1.3

dotnet add package PiiNet --version 0.1.3
                    
NuGet\Install-Package PiiNet -Version 0.1.3
                    
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.3" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="PiiNet" Version="0.1.3" />
                    
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.3
                    
#r "nuget: PiiNet, 0.1.3"
                    
#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.3
                    
#: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.3
                    
Install as a Cake Addin
#tool nuget:?package=PiiNet&version=0.1.3
                    
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.
  • A card is only extracted from a contiguous run or across whitespace/dash boundaries, never from the middle of one long unseparated digit run. Whitespace bridging still catches double-spaced and line-wrapped cards, and when a bridged run is not itself a single valid card (a PAN followed by an amount, reference, or CVV column, or two PANs side by side) the card-length Luhn-valid windows aligned to those boundaries are extracted so the real card redacts while the trailing digits are left intact. What is deliberately not done is sub-windowing a single contiguous digit run: a Luhn-valid 16-digit window buried inside a longer unseparated reference number is left alone, because roughly 10% of random digit runs pass Luhn and blindly windowing would over-redact. If a PAN is concatenated with no separator to other digits, put a non-digit token between the fields.
  • 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 found by a single linear hand-scan and 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, including the colon-hex vector (long IPv6/MAC/config-style blobs), which the IPv6 candidate scan now walks once instead of matching with a backtracking regex. That claim is scoped to what the committed tests actually assert (Release, .NET 8; each test pins a doubling ratio 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.
  • Colon-hex blobs (ColonHexLinearityTests): a 256 KB abcd:-repeated blob scans in tens of milliseconds; the pre-fix candidate regex took roughly four seconds at 128 KB and grew quadratically.

Defense in depth: every source-generated regex now carries a conservative match timeout. A timeout surfaces as a RegexMatchTimeoutException out of Scan/Redact (fail loud) and as false from the Try* variants; it is never swallowed in a way that returns the original, unredacted text as if it were clean. The linear scanners mean the timeout should never actually fire.

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