{"id":51933720,"url":"https://github.com/adamy/redactwire","last_synced_at":"2026-07-28T12:01:03.805Z","repository":{"id":366946535,"uuid":"1275700709","full_name":"adamy/RedactWire","owner":"adamy","description":"Culture-aware PII detection, validation \u0026 redaction for .NET. Checksum-validated rules for 58 countries — SSN, Aadhaar, NRIC, MyKad, CPF, CURP, 身份证, マイナンバー, 주민등록번호, ИНН and more. JSON/XML/object scanning, severity-driven redaction.","archived":false,"fork":false,"pushed_at":"2026-06-26T22:29:48.000Z","size":1129,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-07-28T04:07:27.195Z","etag":null,"topics":["aadhaar","anonymization","ccpa","compliance","cpf","csharp","data-masking","data-protection","dlp","dotnet","gdpr","hipaa","national-id","netstandard","nric","pii","pii-detection","privacy","redaction","ssn"],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/adamy.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-21T03:13:25.000Z","updated_at":"2026-06-26T22:29:52.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/adamy/RedactWire","commit_stats":null,"previous_names":["adamy/redactwire"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/adamy/RedactWire","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adamy%2FRedactWire","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adamy%2FRedactWire/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adamy%2FRedactWire/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adamy%2FRedactWire/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/adamy","download_url":"https://codeload.github.com/adamy/RedactWire/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/adamy%2FRedactWire/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":35990700,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-07-20T02:08:10.276Z","status":"online","status_checked_at":"2026-07-28T02:00:06.341Z","response_time":109,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["aadhaar","anonymization","ccpa","compliance","cpf","csharp","data-masking","data-protection","dlp","dotnet","gdpr","hipaa","national-id","netstandard","nric","pii","pii-detection","privacy","redaction","ssn"],"created_at":"2026-07-28T12:01:02.806Z","updated_at":"2026-07-28T12:01:03.793Z","avatar_url":"https://github.com/adamy.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"# RedactWire\n\n[![NuGet](https://img.shields.io/nuget/v/RedactWire.svg)](https://www.nuget.org/packages/RedactWire/)\n[![Downloads](https://img.shields.io/nuget/dt/RedactWire.svg)](https://www.nuget.org/packages/RedactWire/)\n[![CI](https://github.com/adamy/RedactWire/actions/workflows/ci.yml/badge.svg)](https://github.com/adamy/RedactWire/actions/workflows/ci.yml)\n[![License: Apache 2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](LICENSE)\n\nA lightweight, culture-aware **PII detection, validation and redaction** library for .NET.\n\nRedactWire is regex-first and checksum-validated: it doesn't just match patterns, it\n*verifies* them (Luhn for cards, mod-97 for IBAN, area/group rules for US SSNs), so a\nrandom 16-digit number isn't reported as a credit card. Rules are grouped per culture,\nresults tell you which checks ran and which passed, and every match carries a severity\nthat drives overlap resolution.\n\n- **Target:** `netstandard2.0` — works on .NET Framework 4.6.1+, .NET Core, .NET 5–8+,\n  Mono, Unity, Xamarin.\n- **Dependencies:** one, contracts-only (`Microsoft.Extensions.DependencyInjection.Abstractions`),\n  so the DI bootstrap ships in the box. No other runtime dependencies.\n- **License:** Apache 2.0.\n\n## Features\n\n- Invariant (country-agnostic) rules: **Email**, **Credit card** (Luhn), **IPv4**,\n  **IBAN** (mod-97).\n- **Secret detection** — API keys \u0026 tokens (OpenAI, AWS, GitHub, Stripe, Slack, Google,\n  SendGrid, npm), **JWT**, and **PEM private keys**. Opt-in via `AddSecretDetection()`\n  (on by default in the static `Redactor`). Ideal for scrubbing AI/LLM logs.\n- **Built-in packs for 50+ countries** (see [Coverage](#coverage)) — national IDs, tax\n  numbers, phones, passports, postcodes — most gated by the real check digit/algorithm.\n- **Region-based resolution:** a country's languages share one pack (`en-IN`/`hi-IN`/`ta-IN`,\n  `en-CA`/`fr-CA`, `de-CH`/`fr-CH`/`it-CH`, Singapore's four official languages), while the\n  same language across different countries stays distinct (`zh-CN`/`zh-TW`/`zh-HK`/`zh-SG`).\n- **Validation:** `Validate(value, [culture,] type)` → `Valid` / `Invalid` / `Unsupported`.\n- **Checksum gating:** a failed checksum drops the candidate — it is not reported as\n  low-confidence noise.\n- **Severity model** (`Critical \u003e High \u003e Medium \u003e Low`): the primary key for overlap\n  resolution — a higher-severity match wins over an overlapping lower-severity one,\n  even at lower confidence.\n- **Structured scanning** (opt-in **`RedactWire.Structured`** add-on): detect PII inside\n  **JSON**, **XML**, and **object graphs**, each hit located by JSONPath / XPath / property\n  path. Kept out of core so plain-string users don't pull `System.Text.Json`.\n- **Redaction:** mask (length-preserving), remove, type-label, or a custom replacement.\n- **Honest \"no PII\":** a requested culture with no rule pack is flagged\n  `Supported = false` instead of silently \"passing\".\n- **Extensible:** add your own rules per culture without forking (implement `IPiiRule`,\n  or just use `RegexRule`).\n\n## Coverage\n\nInvariant (every culture): **Email**, **Credit card**, **IPv4**, **IBAN**.\n\nBuilt-in country packs (representative culture shown; all languages of a country resolve to\nthe same pack):\n\n| Region | Packs |\n|---|---|\n| Americas | `en-US` `en-CA` `es-MX` `pt-BR` `es-AR` `es-CL` `es-CO` |\n| Europe | `en-GB` `fr-FR` `de-DE` `it-IT` `es-ES` `pt-PT` `nl-NL` `nl-BE` `de-CH` `de-AT` `pl-PL` `cs-CZ` `sk-SK` `hu-HU` `el-GR` `en-IE` `sv-SE` `nb-NO` `da-DK` `fi-FI` `is-IS` `et-EE` `lt-LT` `lv-LV` `sl-SI` `lb-LU` `ru-RU` `tr-TR` |\n| Asia-Pacific | `zh-CN` `zh-HK` `zh-TW` `zh-MO` `ja-JP` `ko-KR` `en-IN` `id-ID` `vi-VN` `th-TH` `en-PH` `en-PK` `bn-BD` `en-SG` `ms-MY` `en-AU` `en-NZ` |\n| Middle East / Africa | `ar-SA` `ar-EG` `fa-IR` `he-IL` `en-NG` `en-ZA` |\n\n`PiiDetectorBuilder.AvailableCultures` returns this list at runtime. Each pack is documented\nunder [`docs/rules/localized/`](docs/rules/localized/), with verification status in\n[`docs/rules/VERIFICATION.md`](docs/rules/VERIFICATION.md).\n\n**Identifiers (searchable, incl. local names):**\nSSN · SIN · NINO · NHS · TFN · Medicare · IRD · NRIC · MyKad · Aadhaar · PAN · CPF · CNPJ ·\nCURP · RFC · Steuer-ID · NIR · BSN · DNI/NIE · Codice Fiscale · NIF · PESEL · Personnummer ·\nFødselsnummer · HETU · CPR · RRN · My Number · HKID ·\n身份证 · 居民身份证 · 身分證 · 香港身份證 · マイナンバー · 個人番号 · 주민등록번호 ·\nИНН · СНИЛС · บัตรประชาชน · ΑΦΜ · ΑΜΚΑ · आधार · CCCD.\n\n\u003e Patterns and checksums are being verified against authoritative sources (see the\n\u003e verification log). Treat unverified packs as best-effort until confirmed.\n\n## Install\n\n```bash\ndotnet add package RedactWire\n```\n\n## Quick start\n\nThree ways to use it — pick what fits your app.\n\n### 1. Static facade (zero bootstrap)\n\n```csharp\nusing RedactWire;\n\nPiiResult r = Redactor.Detect(\"My SSN is 123-45-6789\");\nbool hasPii  = Redactor.HasPii(\"Call (415) 555-0132\");\nstring clean = Redactor.Redact(\"Card 4242 4242 4242 4242\");\n```\n\n### 2. Builder (manual configuration)\n\n```csharp\nusing System.Globalization;\nusing RedactWire;\n\nvar detector = PiiDetectorBuilder.CreateDefault()      // invariant rules\n    .AddCulture(new CultureInfo(\"en-US\"))              // en-US pack\n    .UseOverlapStrategy(OverlapStrategy.KeepHighestConfidence)\n    .Build();\n\nPiiResult result = detector.Detect(\"Email john@x.co.nz, SSN 123-45-6789\");\nforeach (var m in result.AllMatches)\n    Console.WriteLine($\"{m.Severity} {m.Type} '{m.Value}' conf={m.Confidence:0.00}\");\n```\n\n### 3. Dependency injection (ASP.NET Core / generic host)\n\n```csharp\n// Program.cs\nbuilder.Services.AddRedactWire(b =\u003e b.AddCulture(new CultureInfo(\"en-US\")));\n\n// then inject PiiDetector anywhere\npublic class MyService(PiiDetector detector) { /* detector.Detect(...) */ }\n```\n\n## Results\n\n`Detect` returns a `PiiResult`:\n\n- `Invariant` — `CulturePiiResult` of country-agnostic matches (always evaluated).\n- `Cultures` — one `CulturePiiResult` per requested culture, each with:\n  - `Matches`, `RulesEvaluated`\n  - `Supported` — was a rule pack found for this culture?\n  - `Passed` — `Supported \u0026\u0026 Matches.Count == 0`\n- `HasPii`, `AllMatches` (flat view).\n\nEach `PiiMatch` carries `Type, Value, Start, Length, Confidence, Rule, Culture, Severity`.\n\n## Redaction\n\n```csharp\nvar r = Redactor.Detect(\"SSN 123-45-6789\");\nr.Redact();                                                  // \"SSN ***********\"\nr.Redact(new RedactionOptions { Mode = RedactionMode.Label }); // \"SSN [SocialSecurity]\"\nr.Redact(new RedactionOptions { Custom = m =\u003e $\"\u003c{m.Type}\u003e\" });\n```\n\n## Validation\n\nCheck whether a string is, in full, a valid PII item of a given type — same rules and\nchecksums as detection. Returns `Valid` / `Invalid` / `Unsupported` (the last kept\ndistinct so \"no rule for this type/culture\" isn't mistaken for \"invalid\").\n\n```csharp\nRedactor.Validate(\"123-45-6789\", PiiType.SocialSecurity);            // Valid\nRedactor.Validate(\"123-45-0000\", PiiType.SocialSecurity);            // Invalid\nRedactor.Validate(\"110101199001010015\",\n    new CultureInfo(\"zh-CN\"), PiiType.NationalId);                   // Valid (GB11643)\nRedactor.Validate(\"x\", new CultureInfo(\"fr-FR\"), PiiType.NationalId); // Unsupported\n\n// PiiType.Custom needs a subtype:\ndetector.Validate(\"112-233-445 95\", new CultureInfo(\"ru-RU\"), PiiType.Custom, \"SNILS\");\n```\n\nThe no-culture overload validates against the detector's configured cultures; pass a\n`CultureInfo` to target one. Must be a full-string match (surrounding whitespace ignored).\n\n## Secret detection\n\nAPI keys, tokens and private keys — country-agnostic, so they run as invariant rules.\n\n```csharp\nvar d = PiiDetectorBuilder.CreateDefault().AddSecretDetection().Build();\nforeach (var m in d.Detect(\"OPENAI_API_KEY=sk-abc...xyz\").AllMatches.Where(m =\u003e m.Type == PiiType.Secret))\n    Console.WriteLine(m.Subtype);   // OpenAiKey\n\nRedactor.Redact(\"aws key AKIAIOSFODNN7EXAMPLE\");   // \"aws key ***************...\" (secrets on by default)\n```\n\nCovers OpenAI, AWS access-key-id, GitHub, Stripe, Slack, Google, SendGrid, npm, JWT and PEM\nprivate keys (high-precision, provider-prefixed). AWS *secret* keys / Azure keys / connection\nstrings need entropy/context and are a later phase. See [`docs/rules/secrets.md`](docs/rules/secrets.md).\n\n## Structured scanning (JSON / XML / objects)\n\nAdd-on package — keeps `System.Text.Json` out of the core:\n\n```bash\ndotnet add package RedactWire.Structured\n```\n\nScan structured data and get each match with its location. `DetectJson/DetectXml/DetectObject`\nare extension methods on `PiiDetector`, so they light up once the add-on is referenced\n(use `Redactor.Default.DetectJson(...)` for the shared static detector).\n\n```csharp\nvar detector = PiiDetectorBuilder.CreateDefault().AddCulture(new CultureInfo(\"en-US\")).Build();\n\nforeach (var h in detector.DetectJson(\"\"\"{\"user\":{\"email\":\"a@b.com\"},\"ssn\":\"123-45-6789\"}\"\"\"))\n    Console.WriteLine($\"{h.Path}: {h.Match.Type}\");\n// $.user.email: Email\n// $.ssn: SocialSecurity\n\ndetector.DetectXml(\"\u003cu email=\\\"a@b.com\\\"/\u003e\");   // -\u003e /u/@email\ndetector.DetectObject(myPoco);                  // -\u003e User.Contacts[0].Phone\n```\n\nNotes:\n- Only **string** values are scanned (a number that lost its formatting isn't reliable PII).\n- XML is parsed with DTD processing prohibited and no external resolver — **XXE-safe**.\n- Object scanning walks public properties/fields, handles collections/dictionaries,\n  detects cycles, caps depth, and does not recurse into framework types.\n- The add-on uses `System.Text.Json` (JSON) and BCL `System.Xml` / reflection (XML, objects).\n  This is the only reason `System.Text.Json` is split out of the core package.\n\n## Extending\n\nAdd a custom rule without touching the library. Most needs are covered by `RegexRule`:\n\n```csharp\nvar detector = PiiDetectorBuilder.CreateDefault()\n    .AddCulture(new CultureInfo(\"en-US\"))\n    .AddRule(new CultureInfo(\"en-US\"),                 // bind to one culture\n        new RegexRule(\"EmployeeId\", PiiType.NationalId,\n            @\"(?\u003cv\u003e\\bEMP\\d{6}\\b)\",\n            baseConfidence: 0.8,\n            severity: PiiSeverity.Critical))           // optional; defaults from the type\n    .AddInvariantRule(myGlobalRule)                    // always-on, culture-agnostic\n    .Build();\n```\n\nFor anything beyond a single regex, implement `IPiiRule`. A rule only reports raw\n`RuleHit`s — the engine stamps the culture, the rule id, and the severity default, so\nthere's no plumbing to get wrong:\n\n```csharp\npublic sealed class MyRule : IPiiRule\n{\n    public string Name =\u003e \"MyRule\";\n    public PiiType Type =\u003e PiiType.NationalId;\n\n    public IEnumerable\u003cRuleHit\u003e Find(string text)\n    {\n        // ... locate a match ...\n        yield return new RuleHit(value, start, length, confidence: 0.9);\n        // (pass a PiiSeverity to override the type default)\n    }\n}\n\nvar detector = PiiDetectorBuilder.CreateDefault()\n    .AddCulture(new CultureInfo(\"en-US\"))\n    .AddCulture(new CultureInfo(\"en-GB\"))\n    .AddRule(new MyRule())   // bind to every configured culture at once\n    .Build();\n```\n\n### Remove or replace a built-in rule\n\n```csharp\nPiiDetectorBuilder.CreateDefault()\n    .AddCulture(new CultureInfo(\"en-US\"))\n    .AddSecretDetection()\n    .RemoveRule(PiiType.PostalCode)              // drop ZIP/postcode rules (too noisy)\n    .RemoveRule(PiiType.Secret, \"OpenAiKey\")     // drop one secret type, keep the rest\n    .ReplaceInvariantRule(new RegexRule(\"Email\", PiiType.Email, myStricterEmailPattern))\n    .ReplaceRule(new CultureInfo(\"en-US\"), myUsPhoneRule)   // override by rule Name\n    .Build();\n```\n\n`RemoveRule(type[, subtype])` removes built-in or custom rules everywhere; `ReplaceRule` /\n`ReplaceInvariantRule` swap a rule sharing the same `Name`. Call them after the `Add*` that\nloaded the rules.\n\n### Custom PII types\n\n`PiiType` is an enum (enums can't be extended). For a type that isn't in the list, use\n`PiiType.Custom` and give it a real name via `RuleHit.Subtype` — the name flows into the\nmatch and into redaction labels:\n\n```csharp\nyield return new RuleHit(value, start, length, 0.9,\n    Severity: PiiSeverity.Critical, Subtype: \"NhiNumber\");\n// match.Type == PiiType.Custom, match.Subtype == \"NhiNumber\"\n// Redact(Label)  -\u003e  \"[NhiNumber]\"\n```\n\n## Scope \u0026 roadmap\n\n- **Names and addresses by NER** are out of the core (regex floods false positives). The\n  `Address` rule is a regex heuristic; full name detection is reserved for an optional\n  `RedactWire.Ner` package (local ONNX, e.g. GLiNER) — kept separate to avoid forcing a\n  large model + its license on every consumer.\n- More country packs to come (UK, DE/FR, AU/JP/SG/IN, …).\n\n## Projects\n\n| Path | What |\n|---|---|\n| `src/RedactWire` | the core library (detection / validation / redaction + DI bootstrap) |\n| `src/RedactWire.Structured` | JSON/XML/object scanning add-on (`System.Text.Json`) |\n| `src/RedactWire.Cli` | command-line scanner / redactor |\n| `samples/RedactWire.Sample.Web` | ASP.NET Core Razor Pages PII tester |\n| `tests/RedactWire.Tests` | xUnit tests |\n| `docs/rules/` | per-rule documentation (`common.md`, `severity.md`, `localized/`) |\n\n## CLI\n\n```bash\necho \"My SSN is 123-45-6789\" | dotnet run --project src/RedactWire.Cli\ndotnet run --project src/RedactWire.Cli -- --redact --text \"Card 4242 4242 4242 4242\"\n```\n\nExit code is `1` when PII is found, `0` when clean — handy in CI.\n\n## License\n\nApache License 2.0. See [LICENSE](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fadamy%2Fredactwire","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fadamy%2Fredactwire","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fadamy%2Fredactwire/lists"}