Skip to main content
CID222 Docs

PII detection

The personal-data entity types CID222 detects, how regex and the NER model divide the work, and how a detection becomes a placeholder.

  • Version: 0.4
  • Role: admin_user, normal_user, viewer

CID222 detects personal data in a prompt before it reaches a provider, and in a reply before it reaches the user. Two layers do the work: deterministic patterns with checksums, and a multilingual ONNX NER model.

The problem

Personal data does not arrive labelled. It arrives inside a paragraph a person typed, in a language you did not choose, sometimes as a scan of a document. A pattern catches what has a shape — a card number, an IBAN, a national ID — and misses a name it has never seen. A model catches the name and cannot prove the card number is real. Running only one of them means either leaking or blocking the work; running both means reconciling two opinions about the same fifteen characters.

How CID222 does it

Two layers, one catalogue

The regex layer loads its patterns from the input-filter rows in the database, so the catalogue is data an administrator can extend rather than code. The NER layer is a single multilingual ONNX DistilBERT model that emits 21 native labels. Both layers report against the same entity type where the concept overlaps, so detections of the same span merge instead of double-counting.

What is detected

Every row below is a shipped filter. Default action is what the seed sets; an administrator changes it per rule, and a per-tenant or per-group override can change it for one department without touching the rule.

Personal identifiers

EntityDescriptionDetected byDefault action
PERSON_NAMEPerson namesNER, plus one regex for uppercase names on Turkish ID cardsmask
EMAILEmail addressesRegex, NERmask
PHONEPhone numbers — US, international, Turkish mobile and landline, six patternsRegex, NERmask
SSNUS Social Security numberRegex with the us.ssn checksum, NERmask
TC_KIMLIK, ID_NUMBERTurkish national ID, including OCR-tolerant and context-anchored formsRegex with the tr.tckimlik checksum, NERmask
ID_DOCUMENT_NOTurkish ID card document and serial numberRegexmask
PASSPORTPassport numbers — US, generic EU, TurkishRegex, NERmask
DATE_TIMEDates and timesNER onlyflag

Financial data

EntityDescriptionDetected byDefault action
CREDIT_CARDCard numbers, plain, spaced and dottedRegex with Luhn plus brand and length validation, NERmask
IBAN, ACCOUNT_NUMBERIBAN, Turkish IBAN and Turkish domestic account numbersRegex with ISO 13616 mod-97, NERmask
VKN, TAX_IDTurkish tax numberRegex with the tr.vkn checksummask
CRYPTO_ADDRESSCrypto wallet addressesNER onlymask

Location and network data

EntityDescriptionDetected byDefault action
LOCATION, STREET_ADDRESS, CITY, ZIPCODEAddresses, cities, regions, postal codesNER, plus a Turkish postal-code regex with a province-range checkflag
IP_ADDRESSIPv4 and IPv6Regex, NERmask
LICENSE_PLATETurkish vehicle plates, province-range checkedRegex, NERmask
URLURLsNER onlyflag
ORGANIZATIONCompany and institution namesNER onlyflag
mac_colon, mac_hyphenMAC addressesRegex onlymask

Health data

EntityDescriptionDetected byDefault action
mrn_labeled, mrn_formatMedical record numbers, labelled and bare formatsRegex onlymask

Warning

Medical record numbers and MAC addresses are regex-only rules with no model label, so their type in an API response is the pattern name shown above, not a tidy MRN or MAC_ADDRESS. Their value still carries the placeholder — [MRN], [MAC_ADDRESS]. Match on the placeholder if you need a stable category. There is no broader health-data detector: CID222 does not classify conditions, medications or diagnoses.

Note

Beyond personal data, a separate DLP rule set detects leaked secrets — API keys, passwords, private keys, database connection strings, OAuth and refresh tokens, webhook secrets, CVE identifiers. The full catalogue is on the content detection page.

Validation before trust

A detection is not acted on until it survives validation. Rules come in eight categories — length, boundary, context, confidence, pattern, content, checksum and intent_gate — and are attached to filter rows as data.

The checksum category is the strongest of them and returns a three-state verdict:

VerdictMeaningEffect
validRight shape and the check passedThe detection stands
invalidRight shape, check failedDropped, or demoted from mask to flag where the rule says so
unknownNot this validator's shape at allTreated as if no validator ran

That third state matters more than it looks. A national-ID field also carries document numbers such as B1234567, and a bank-account field also carries bare account numbers. A digit-only checksum cannot read either, and failing open is what stops it from silently discarding them.

A validated entity passes only if its final confidence reaches 0.6. A detection is boosted by 0.1, clamped at 1.0, when a context keyword registered for its entity type appears within 50 characters of the span — the email rule registers contact and communication, for example. The keyword-to-type map is loaded from the active filter rows.

Masking

A masked entity is replaced by a placeholder, and the placeholder comes from one of two places.

  • A regex rule uses its own replacement_text. [EMAIL], [PHONE], [SSN], [CREDIT_CARD], [IBAN], [NATIONAL_ID], [DOCUMENT_NO], [PASSPORT], [LICENSE_PLATE], [POSTAL_CODE], [BANK_ACCOUNT], [TAX_ID], [NAME], [IP_ADDRESS], [MRN].
  • A model detection with no rule-supplied action gets a generated placeholder: the entity type with underscores as spaces, four asterisks appended. PERSON_NAME becomes [PERSON NAME****].

Neither of those is what the provider finally sees. The masking step takes that base label and numbers it per distinct value, so the text that leaves the gateway carries [EMAIL_1], [EMAIL_2], [PERSON_NAME_1]. The number is stable for the whole conversation: one address stays [EMAIL_1] from turn one to turn ten. See pseudonymization and masking.

Original: "Contact john.smith@company.com or call 555-123-4567"
Masked:   "Contact [EMAIL_1] or call [PHONE_1]"

Original: "My SSN is 123-45-6789 and my card is 4111-1111-1111-1111"
Masked:   "My SSN is [SSN_1] and my card is [CREDIT_CARD_1]"

Original: "Send it to John Smith, then mail Jane Doe"
Masked:   "Send it to [PERSON_NAME_1], then mail [PERSON_NAME_2]"

The masked text is what the provider receives. Where reversible masking is configured, the mapping back to the original is encrypted per session with AES-256-GCM; without the key, responses stay masked and no mapping is written at all.

Adding your own patterns

An organisation-specific identifier is a new filter row with one or more regex patterns. The shape is the same as the shipped rules:

{
  "filter_name": "employee-id",
  "filter_type": "pii_detection",
  "action_type": "mask",
  "priority": 100,
  "ml_entity_types": ["EMPLOYEE_ID"],
  "validation_entity_type": "employee_id",
  "context_types": ["staff", "personnel"],
  "is_active": true,
  "regex_patterns": [
    {
      "pattern_name": "employee_id",
      "regex_pattern": "EMP-[0-9]{6}",
      "replacement_text": "[EMPLOYEE_ID]",
      "description": "Internal employee identifier"
    }
  ]
}

ml_entity_types[0] becomes the type reported to callers. context_types are the keywords that earn the confidence boost. Test the pattern with the dashboard's filter-testing tool before you activate it.

Limits and known gaps

  • Accuracy is conditional and is not one number. It varies with entity type, language, text length and filter configuration. Structured identifiers with a checksum behave close to deterministically; names, locations and organisations depend on the NER model and its context. Quote the qualified figures in performance and accuracy, never a bare percentage.
  • Detection is CID222's own models and rules. There is no Presidio, no spaCy, and no OPA/Rego policy engine anywhere in the path. The catalogue is filter rows in PostgreSQL and one ONNX NER model.
  • Free-text entities are flagged, not masked, by default. LOCATION, ORGANIZATION, DATE_TIME and URL ship as flag rules, because masking them at NER precision breaks ordinary prompts. If you need them masked, change the action deliberately and measure the false positives you accept.
  • Turkish coverage is deeper than any other national coverage. Turkish national ID, tax number, IBAN, domestic account, postal code, licence plate and ID-card document number all have dedicated patterns and validators. Another country's identifiers have none unless you add them.
  • A broken pattern protects nothing, silently. A stored regex that fails to compile is skipped at load time. The gateway logs it loudly and marks the filter broken in the dashboard, but the filter still reads as active.
  • The pattern cache is up to five minutes stale. The regex layer refreshes its compiled patterns on a five-minute TTL, so a new or edited rule is not necessarily live on the next request.
  • No health-data classifier exists. Medical record numbers are matched by two regex patterns. Conditions, medications and diagnoses in free text are not detected as health data.
  • PII inside a fragmented upload is not inspected. Nothing buffers a body across requests, so chunked and resumable uploads are refused rather than scanned.

Last updated on

On this page

Download PDF