| Title: | Detect Indian Personal Data Before It Reaches Language Models |
|---|---|
| Description: | Detects personal data in text before it is sent to a large language model, with Indian identifiers built in: Aadhaar (validated with the Verhoeff checksum), Aadhaar Virtual ID, PAN, GSTIN, voter ID, passport, driving licence, CKYC, ration card and job card numbers, UPI IDs, IFSC codes, bank account and payment card numbers with expiry dates and verification codes, mobile numbers and Ayushman Bharat (ABHA) health IDs, plus rule-based detection of names and addresses in Latin and Indic scripts. Candidates are validated with checksums, scored from surrounding context and matched through Indic digits and invisible characters. The rules come from a language-neutral specification shared with the 'Python' package of the same name. |
| Authors: | Bharat Sikka [aut, cre, cph], Aishwarya Bais [aut, cph] |
| Maintainer: | Bharat Sikka <[email protected]> |
| License: | Apache License (>= 2) |
| Version: | 0.1.0.9000 |
| Built: | 2026-11-02 00:59:17 UTC |
| Source: | https://github.com/dwarpal/credgate |
The match may not be preceded or followed by a letter or digit, nor by a digit joined with a space or hyphen (so a 12-digit window inside a 16-digit card number is not reported as an Aadhaar number).
bounded(core)bounded(core)
core |
An ICU regular expression. |
A regular expression.
p <- bounded("[0-9]{4}") stringi::stri_detect_regex(c("PIN 1234", "card 12345678"), p)p <- bounded("[0-9]{4}") stringi::stri_detect_regex(c("PIN 1234", "card 12345678"), p)
BigQuery samples the table and only the sample is downloaded; only the
columns credgate can scan are read, and a dry run checks the cost of
every query first. credgate makes no network calls of its own: it uses
bigrquery, with your credentials (bigrquery::bq_auth()). The rules are
shared with the Python package (credgate.bigquery; see the spec's
"BigQuery tables").
bq_scan_pii( table, billing = NULL, sample = 10000L, min_rate = 0, detectors = NULL, sampling = "tablesample", max_bytes = 1e+09 ) bq_pii_inventory( table, source = NULL, billing = NULL, sample = 10000L, min_rate = 0, detectors = NULL, sampling = "tablesample", max_bytes = 1e+09 ) bq_inventory_dataset( dataset, source = NULL, billing = NULL, sample = 10000L, min_rate = 0, detectors = NULL, sampling = "tablesample", max_bytes = 1e+09 )bq_scan_pii( table, billing = NULL, sample = 10000L, min_rate = 0, detectors = NULL, sampling = "tablesample", max_bytes = 1e+09 ) bq_pii_inventory( table, source = NULL, billing = NULL, sample = 10000L, min_rate = 0, detectors = NULL, sampling = "tablesample", max_bytes = 1e+09 ) bq_inventory_dataset( dataset, source = NULL, billing = NULL, sample = 10000L, min_rate = 0, detectors = NULL, sampling = "tablesample", max_bytes = 1e+09 )
table |
A table: |
billing |
Project billed for the queries; defaults to the table's project. |
sample |
Maximum number of rows sampled. |
min_rate, detectors
|
As for |
sampling |
|
max_bytes |
Refuse any query estimated to process more bytes than this (default 1 GB). Also sent to BigQuery as the billing limit, except with TABLESAMPLE, where BigQuery would check it against the whole table. |
source |
Source name for the inventory; defaults to
|
dataset |
A dataset: |
bq_scan_pii() a scan report as from scan_pii();
bq_pii_inventory() and bq_inventory_dataset() an inventory as from
pii_inventory(), with the table's row count and sample_method
"all", "bigquery-tablesample" or "bigquery-rand".
## Not run: bigrquery::bq_auth() bq_scan_pii("my-project.crm.customers") inv <- bq_inventory_dataset("my-project.crm") write_inventory(inv, "inventory.csv") ## End(Not run)## Not run: bigrquery::bq_auth() bq_scan_pii("my-project.crm.customers") inv <- bq_inventory_dataset("my-project.crm") write_inventory(inv, "inventory.csv") ## End(Not run)
cg_audit_log() opens an append-only, hash-chained log; pass it to
policy_apply() (audit = log) to record every decision, allowed or
blocked, or record a decision yourself with audit_record(). Each record
says who sent what kind of data where, under which policy revision and
rule - never the text or the values. With an audit key (key or
CREDGATE_AUDIT_KEY; not the token key), identifiers are recorded as keyed
hashes, so whoever holds the key can check whether a given identifier was
involved. Card verification codes are never hashed.
cg_audit_log(path, key = NULL, actor = NULL) audit_record(log, decision) audit_verify(path) audit_read(path)cg_audit_log(path, key = NULL, actor = NULL) audit_record(log, decision) audit_verify(path) audit_read(path)
path |
The log file (JSON Lines); created if missing. |
key |
Audit key (64+ hex digits or raw); |
actor |
Who makes the requests; defaults to the operating-system user. |
log |
A log from |
decision |
A decision from |
audit_verify() checks that no record was edited, deleted, inserted or
reordered; keep copies of the returned head elsewhere to detect records
removed from the end. audit_read() returns the records. The same files
are written and verified by the Python package. Needs the openssl package.
cg_audit_log(): a log. audit_record(): the record (a list),
invisibly. audit_verify(): a list with ok, records, head, and for
a broken log the failing line and the error. audit_read(): a data
frame, one row per record, with the items as a list column.
policy <- cg_policy(list( policy = "credgate-policy", version = 1L, id = "example", revision = 1L, destinations = list(claude = list(tier = "frontier", provider = "anthropic")), rules = list(list(id = "no-ids", action = "redact", when = list(classes = list("identity_strong")))), default_action = "redact" )) path <- tempfile(fileext = ".jsonl") log <- cg_audit_log(path, actor = "analyst1") policy_apply(policy, "PAN ABCPE1234F", "claude", audit = log) audit_verify(path) audit_read(path)[, c("seq", "actor", "destination", "allowed")]policy <- cg_policy(list( policy = "credgate-policy", version = 1L, id = "example", revision = 1L, destinations = list(claude = list(tier = "frontier", provider = "anthropic")), rules = list(list(id = "no-ids", action = "redact", when = list(classes = list("identity_strong")))), default_action = "redact" )) path <- tempfile(fileext = ".jsonl") log <- cg_audit_log(path, actor = "analyst1") policy_apply(policy, "PAN ABCPE1234F", "claude", audit = log) audit_verify(path) audit_read(path)[, c("seq", "actor", "destination", "allowed")]
List registered detectors
cg_detectors()cg_detectors()
A data frame with one row per detector.
cg_detectors()cg_detectors()
Measures how well detect_pii() finds the PII in documents where every
PII span has been labelled: true positives, false positives, false
negatives, precision, recall and F1, per type and overall. Use it to
check credgate on your own (labelled) data. The rules are shared with the
Python package (see the spec's "Evaluation" section).
cg_evaluate( documents, min_confidence = 0, match = c("exact", "overlap"), detectors = NULL )cg_evaluate( documents, min_confidence = 0, match = c("exact", "overlap"), detectors = NULL )
documents |
Labelled documents: a list of lists with |
min_confidence |
Ignore predictions scoring below this value. |
match |
|
detectors |
Detector ids to run; |
A data frame of class cg_evaluation with columns type, tp,
fp, fn, precision, recall and f1: one row per type, then
overall (micro-averaged). Rates are NA when undefined.
docs <- list( list(text = "PAN: ABCPE1234F", spans = list(list(start = 5, end = 15, type = "pan"))), list(text = "order no 878245671885", spans = list()) ) cg_evaluate(docs) cg_evaluate(docs, min_confidence = 0.5)docs <- list( list(text = "PAN: ABCPE1234F", spans = list(list(start = 5, end = 15, type = "pan"))), list(text = "order no 878245671885", spans = list()) ) cg_evaluate(docs) cg_evaluate(docs, min_confidence = 0.5)
cg_new_token_key() makes a new random key (64 hex digits) for
cg_vault(). cg_key_fingerprint() returns 16 hex digits that identify a
key without revealing it; saved keyed vaults record it, so opening one with
the wrong key fails. Both need the openssl package.
cg_new_token_key() cg_key_fingerprint(key = NULL)cg_new_token_key() cg_key_fingerprint(key = NULL)
key |
A key as hex digits or a raw vector; |
A string.
key <- cg_new_token_key() cg_key_fingerprint(key)key <- cg_new_token_key() cg_key_fingerprint(key)
Applies NFKC normalisation, maps Indic and Arabic-Indic digits to ASCII,
maps dash variants to - and removes invisible characters (zero-width
space/joiners, BOM, soft hyphen). Returns the normalised text together
with an offset map back to the original.
cg_normalise(x)cg_normalise(x)
x |
A character vector. |
A list with one element per element of x, each a list with
text (the normalised string) and map (an integer vector giving, for
each code point of text, the index of the original code point it came
from, or NULL when the text is unchanged).
cg_normalise("Aadhaar: \u0968\u0969\u096a\u096b")cg_normalise("Aadhaar: \u0968\u0969\u096a\u096b")
A policy decides, for one request to one destination, what happens to each identifier found in it: let it through, redact it, tokenise it, or block the whole request. Policies are YAML files written by InfoSec and compliance; the same file works in R and Python. Everything is checked when the policy is loaded - unknown fields, tiers, actions, types or classes are errors - so a policy that cannot be read correctly is never applied.
cg_policy(x)cg_policy(x)
x |
Path to a policy file (YAML), or the policy as a list. |
Destinations have a tier: local (this machine), internal (the
organisation's own network or hosting) or frontier (an external model
provider). Rules are tried in order and the first that matches an
identifier decides its action; a rule's when can name particular
destinations, tiers, types, classes, and all_of - types that must
all occur in the request (for example a card number with its CVV and expiry
date). Any block blocks the whole request. A destination that is not
listed receives nothing: requests to it are refused.
An object of class cg_policy.
policy_decide(), policy_apply(); the format is described in
the spec (cg_spec_path(), README, "Policies").
policy <- cg_policy(list( policy = "credgate-policy", version = 1L, id = "example", revision = 1L, destinations = list(ollama = list(tier = "local"), claude = list(tier = "frontier")), rules = list( list( id = "card-with-cvv", action = "block", when = list(tiers = list("frontier"), all_of = list("card", "card_cvv")) ), list(id = "in-house", action = "allow", when = list(tiers = list("local"))) ), default_action = "redact" )) policy_apply(policy, "Card 4111 1111 1111 1111, CVV 123", "claude") policy_apply(policy, "PAN ABCPE1234F", "claude")policy <- cg_policy(list( policy = "credgate-policy", version = 1L, id = "example", revision = 1L, destinations = list(ollama = list(tier = "local"), claude = list(tier = "frontier")), rules = list( list( id = "card-with-cvv", action = "block", when = list(tiers = list("frontier"), all_of = list("card", "card_cvv")) ), list(id = "in-house", action = "allow", when = list(tiers = list("local"))) ), default_action = "redact" )) policy_apply(policy, "Card 4111 1111 1111 1111, CVV 123", "claude") policy_apply(policy, "PAN ABCPE1234F", "claude")
Read labelled documents from a JSON Lines file
cg_read_corpus(path)cg_read_corpus(path)
path |
Path of a JSON Lines file with one labelled document per
line (see |
A list of documents.
The spec (detectors, engine parameters and conformance cases) is shared
by every CredGate implementation. See its README.md for the schema.
cg_spec_path()cg_spec_path()
The path of the spec directory.
list.files(cg_spec_path())list.files(cg_spec_path())
A vault holds the mapping between tokens such as [PAN_1] and the values
they stand for. It starts empty and grows as tokenise() meets new
identifiers; the same identifier always gets the same token. The vault
lives in memory only, and printing it masks the values.
cg_vault(key = NULL, keyed = FALSE)cg_vault(key = NULL, keyed = FALSE)
key |
A token key: at least 64 hex digits (32 bytes), or a raw
vector. Make one with |
keyed |
Use keyed tokens with the key from |
With a token key, tokens are keyed ([PAN_4qzm7k2x9tbc3]): the same
identifier gets the same token in every vault with that key - across
sessions, datasets, R and Python, and parallel workers, whose vaults can be
combined with vault_merge(). Without a key, tokens are numbered
([PAN_1]) and belong to this vault only. Keyed tokens need the openssl
package.
Whoever holds the key can confirm a guessed value by computing its token, so keep the key like a password: from a key store or the environment variable, never in code or project files.
An object of class cg_vault. It is modified in place by
tokenise(), so it does not need to be reassigned.
vault <- cg_vault() tokenise("PAN ABCPE1234F", vault) vault key <- cg_new_token_key() a <- cg_vault(key) b <- cg_vault(key) tokenise("PAN ABCPE1234F", a) == tokenise("pan abcpe1234f", b)vault <- cg_vault() tokenise("PAN ABCPE1234F", vault) vault key <- cg_new_token_key() a <- cg_vault(key) b <- cg_vault(key) tokenise("PAN ABCPE1234F", a) == tokenise("pan abcpe1234f", b)
Runs every registered detector over normalised text, validates candidates (checksums, code tables), scores them from nearby context and resolves overlapping matches (the more confident, then the longer, span wins - so a GSTIN is reported once, not also as the PAN inside it).
detect_pii( text, detectors = NULL, context = NULL, min_confidence = 0, window = NULL )detect_pii( text, detectors = NULL, context = NULL, min_confidence = 0, window = NULL )
text |
A character vector. |
detectors |
Detector ids to run; |
context |
Optional extra context per element of |
min_confidence |
Drop matches scoring below this value. |
window |
Number of characters either side of a match searched for
context keywords; |
A data frame of class cg_spans with one row per match:
doc (index into text), start, end (character positions in the
original text), type, class, value, canonical, confidence and
reasons. Printing masks the values.
detect_pii("Customer PAN ABCPE1234F, GSTIN 27AAPFU0939F1ZV")detect_pii("Customer PAN ABCPE1234F, GSTIN 27AAPFU0939F1ZV")
A GSTIN is 15 characters: a 2-digit state code, the holder's PAN, an
entity number, the letter Z and a mod-36 check character.
gstin_valid() checks structure and check character;
gstin_check_char() computes the check character for the first 14
characters.
gstin_valid(x) gstin_check_char(x)gstin_valid(x) gstin_check_char(x)
x |
A character vector. Case is ignored. |
gstin_valid(): a logical vector. gstin_check_char(): a
character vector (NA for invalid input).
gstin_check_char("27AAPFU0939F1Z")gstin_check_char("27AAPFU0939F1Z")
Luhn checksum (payment cards)
luhn_valid(x)luhn_valid(x)
x |
A character vector of digit strings. Spaces and hyphens are ignored. |
A logical vector.
luhn_valid("4111 1111 1111 1111")luhn_valid("4111 1111 1111 1111")
Define a PII detector
new_detector( id, class, label, pattern, capture = NULL, validate = NULL, checks = NULL, base = 0.8, base_without_context = NULL, context = NULL, negative = NULL, context_required = FALSE, canonical = NULL, describe = NULL, ignore_case = FALSE, column_label = NULL, column_pattern = NULL )new_detector( id, class, label, pattern, capture = NULL, validate = NULL, checks = NULL, base = 0.8, base_without_context = NULL, context = NULL, negative = NULL, context_required = FALSE, canonical = NULL, describe = NULL, ignore_case = FALSE, column_label = NULL, column_pattern = NULL )
id |
Unique identifier, e.g. |
class |
Data class used by policies, e.g. |
label |
Human-readable name. |
pattern |
ICU regular expression for candidates, or several (their
matches are combined). Wrap it with |
capture |
Name of a capture group whose span is reported instead of
the whole match, or |
validate |
Function taking the matched strings (normalised text) and
returning a logical vector; candidates returning |
checks |
Short description of what |
base |
Confidence (0-1) of a validated match without supporting
context, or a function taking the matched strings and returning one
confidence per match (e.g. higher for a mobile number written with
|
base_without_context |
Confidence (0-1) used instead of |
context |
Regex (case-insensitive) of keywords that raise confidence
when they appear near the match, or |
negative |
Regex (case-insensitive) of keywords that lower confidence,
or |
context_required |
If |
canonical |
Function mapping matched strings to a canonical form (used for consistent tokenisation). Defaults to removing spaces and hyphens and upper-casing. |
describe |
Optional function returning extra reasons per match (e.g. the PAN holder type). |
ignore_case |
Match the pattern case-insensitively. |
column_label |
Regex (case-insensitive) that, when it matches the
caller's extra context (a column name), makes that context the label for
|
column_pattern |
Patterns used, reporting the whole match, only when
|
An object of class cg_detector.
employee_id <- new_detector( id = "employee_id", class = "internal", label = "Employee ID", pattern = bounded("EMP[0-9]{6}"), context = "employee|staff" ) employee_id$labelemployee_id <- new_detector( id = "employee_id", class = "internal", label = "Employee ID", pattern = bounded("EMP[0-9]{6}"), context = "employee|staff" ) employee_id$label
Scans a table with the same rules as scan_pii(), but records every
column - including those where nothing was found - with the source and
table names, the sample size and method, and when and by what it was
scanned. No values are recorded, not even masked ones. Combine the
inventories of several tables with rbind() and save them with
write_inventory(); the file format is shared with the Python package
(see the spec's "Inventory" section).
pii_inventory( data, source, table, sample = 10000L, min_rate = 0, detectors = NULL, seed = 42L )pii_inventory( data, source, table, sample = 10000L, min_rate = 0, detectors = NULL, seed = 42L )
data |
A data frame, an Arrow |
source |
Name of the data source, chosen by you (e.g.
|
table |
Name of the table within the source (e.g. |
sample |
Maximum number of values checked per column. |
min_rate |
Report a type only if it occurs in at least this share of sampled values (0 reports every type found). |
detectors |
Detector ids to run; |
seed |
Random seed for sampling Spark tables; ignored otherwise. |
A data frame of class cg_inventory with one row per column and
PII type found, and one row with type = NA for each column where none
was found: v, source, table, column, type, class, n_rows
(when known without reading the table), n_sampled, n_matched,
n_full, rate, mean_confidence, sample_method ("all" or
"even", or "spark-sample" for Spark tables), scanned_at (UTC) and
credgate (the version that scanned).
customers <- data.frame( cust_pan = c("ABCPE1234F", "AAACR5055K", "XYZPQ9876R"), notes = c("Called, Aadhaar 542229371182", "no issues", "mobile 9876543210"), balance = c(1520.5, 99.75, 0.25) ) inv <- pii_inventory(customers, source = "crm", table = "customers") inv path <- tempfile(fileext = ".csv") write_inventory(inv, path) read_inventory(path)customers <- data.frame( cust_pan = c("ABCPE1234F", "AAACR5055K", "XYZPQ9876R"), notes = c("Called, Aadhaar 542229371182", "no issues", "mobile 9876543210"), balance = c(1520.5, 99.75, 0.25) ) inv <- pii_inventory(customers, source = "crm", table = "customers") inv path <- tempfile(fileext = ".csv") write_inventory(inv, path) read_inventory(path)
policy_decide() works out what the policy does with each identifier in a
request to destination, without changing the text. policy_apply() also
rewrites the text when the request is allowed: identifiers to redact become
placeholders such as [PAN], identifiers to tokenise become tokens from
vault. A blocked request gets no text: nothing may be sent.
policy_decide(policy, text, destination) policy_apply(policy, text, destination, vault = NULL, audit = NULL)policy_decide(policy, text, destination) policy_apply(policy, text, destination, vault = NULL, audit = NULL)
policy |
A policy from |
text |
The texts of one request (for example the messages of a chat). |
destination |
Name of a destination in the policy; an unknown one is an error. |
vault |
A vault from |
audit |
An audit log from |
An object of class cg_decision: a list with policy_id,
policy_revision, destination, tier, provider, url, allowed,
blocked_by ("destination-blocked" and rule ids), matches (a data frame of the identifiers with their action and
deciding rule, "default" when no rule matched) and text (the
rewritten texts, or NULL).
Shows the redacted text together with each detected identifier (value masked), its confidence and the reasons it was detected - so you can check exactly what would leave the machine before sending anything.
preview(text, detectors = NULL, context = NULL, min_confidence = 0)preview(text, detectors = NULL, context = NULL, min_confidence = 0)
text |
A character vector. |
detectors |
Detector ids to run; |
context |
Optional extra context per element of |
min_confidence |
Drop matches scoring below this value. |
An object of class cg_preview: a list with text (the input),
redacted (as from redact()) and spans (as from detect_pii()).
Printing shows the redacted text and a masked table of the spans.
preview("Name: Priya Nair; PAN: ABCPN1234K; order no 878245671885")preview("Name: Priya Nair; PAN: ABCPN1234K; order no 878245671885")
Replaces every detected identifier with a placeholder naming its type, for
example [PAN] or [PERSON_NAME]. Everything else in the text is kept
exactly. Redaction is irreversible; the rules are shared with the Python
package (see the spec's "Redaction" section).
redact(text, detectors = NULL, context = NULL, min_confidence = 0)redact(text, detectors = NULL, context = NULL, min_confidence = 0)
text |
A character vector. |
detectors |
Detector ids to run; |
context |
Optional extra context per element of |
min_confidence |
Drop matches scoring below this value. |
A character vector the same length as text, with PII replaced.
NA stays NA.
preview() to see what would be redacted and why.
redact("Name: Priya Nair; PAN: ABCPN1234K") redact(c("UPI priya.n@oksbi", "nothing here"))redact("Name: Priya Nair; PAN: ABCPN1234K") redact(c("UPI priya.n@oksbi", "nothing here"))
Writes a copy of a Parquet file with identifiers in the chosen text
columns replaced by placeholders such as [PAN]. The file is read in
record batches, so it may be larger than memory; other columns and the
schema are kept.
redact_parquet(source, destination, columns = NULL, batch_size = 10000L, ...)redact_parquet(source, destination, columns = NULL, batch_size = 10000L, ...)
source, destination
|
Parquet files to read and write; they must differ. |
columns |
Columns to redact; |
batch_size |
Rows per batch. |
... |
Passed to |
The number of rows written, invisibly.
redact_query() for database tables, including DuckDB.
src <- tempfile(fileext = ".parquet") dst <- tempfile(fileext = ".parquet") arrow::write_parquet(data.frame(id = 1:2, notes = c("PAN ABCPE1234F", "no PII")), src) redact_parquet(src, dst) arrow::read_parquet(dst)src <- tempfile(fileext = ".parquet") dst <- tempfile(fileext = ".parquet") arrow::write_parquet(data.frame(id = 1:2, notes = c("PAN ABCPE1234F", "no PII")), src) redact_parquet(src, dst) arrow::read_parquet(dst)
Runs query on a DBI connection, fetches the result chunk_size rows at
a time, redacts the chosen text columns and appends each chunk to the
table to. The result may be larger than memory. Works with any DBI
database; with DuckDB, query can read files directly
("SELECT * FROM 'calls.parquet'"), and COPY to TO 'out.parquet'
writes the redacted table back out.
redact_query( con, query, to, columns = NULL, chunk_size = 10000L, append = FALSE, to_con = con, ... )redact_query( con, query, to, columns = NULL, chunk_size = 10000L, append = FALSE, to_con = con, ... )
con |
A DBI connection. |
query |
SQL query whose result is redacted. |
to |
Name of the table to create (or, with |
columns |
Columns to redact; |
chunk_size |
Rows fetched at a time. |
append |
Append to an existing table |
to_con |
Connection to write |
... |
Passed to |
The number of rows written, invisibly.
con <- DBI::dbConnect(duckdb::duckdb()) DBI::dbWriteTable(con, "calls", data.frame(id = 1:2, notes = c("PAN ABCPE1234F", "no PII"))) redact_query(con, "SELECT * FROM calls", "calls_redacted") DBI::dbReadTable(con, "calls_redacted") DBI::dbDisconnect(con, shutdown = TRUE)con <- DBI::dbConnect(duckdb::duckdb()) DBI::dbWriteTable(con, "calls", data.frame(id = 1:2, notes = c("PAN ABCPE1234F", "no PII"))) redact_query(con, "SELECT * FROM calls", "calls_redacted") DBI::dbReadTable(con, "calls_redacted") DBI::dbDisconnect(con, shutdown = TRUE)
Register a detector
register_detector(detector, overwrite = FALSE)register_detector(detector, overwrite = FALSE)
detector |
A detector created with |
overwrite |
Replace an existing detector with the same id. |
The detector id, invisibly.
register_detector(new_detector( id = "employee_id", class = "internal", label = "Employee ID", pattern = bounded("EMP[0-9]{6}") )) detect_pii("Approved by EMP123456")$typeregister_detector(new_detector( id = "employee_id", class = "internal", label = "Employee ID", pattern = bounded("EMP[0-9]{6}") )) detect_pii("Approved by EMP123456")$type
Replaces tokens made by tokenise() - for example in a model's answer -
with the values they stand for. Tokens are recognised with any letter
case, with spaces inside the brackets ([ pan_1 ]) and without brackets
(PAN_1), because models do not always copy them exactly. Tokens the
vault does not know are left unchanged.
restore(text, vault, strict = FALSE)restore(text, vault, strict = FALSE)
text |
A character vector. |
vault |
The vault used by |
strict |
If |
A character vector like text.
vault <- cg_vault() tokenise("Name: Priya Nair, PAN ABCPE1234F", vault) restore("[PERSON_NAME_1] holds [pan_1].", vault)vault <- cg_vault() tokenise("Name: Priya Nair, PAN ABCPE1234F", vault) restore("[PERSON_NAME_1] holds [pan_1].", vault)
Reports, per column, which PII types occur and in what share of values -
for example that cust_id looks like a PAN in 98% of rows. Useful on its
own, before any data is shared, to find which columns need protection.
scan_pii(data, sample = 10000L, min_rate = 0, detectors = NULL, seed = 42L)scan_pii(data, sample = 10000L, min_rate = 0, detectors = NULL, seed = 42L)
data |
A data frame, an Arrow |
sample |
Maximum number of values checked per column. |
min_rate |
Report a type only if it occurs in at least this share of sampled values (0 reports every type found). |
detectors |
Detector ids to run; |
seed |
Random seed for sampling Spark tables; ignored otherwise. |
Each column is sampled (evenly spaced, so results are reproducible), and
every sampled value is checked with detect_pii() using the column name as
context: a column named voter_id supports Voter ID matches that would
otherwise need a keyword in the text. Whole numbers are checked too
(mobile and account numbers are often stored as numbers); other numbers,
logicals and dates are skipped. The rules are shared with the Python
package (see the spec's "Column scan" section).
A data frame of class cg_scan with one row per column and type:
column, type, class, n_sampled, n_matched (values with a
match), n_full (values that are entirely the identifier), rate
(n_matched / n_sampled), mean_confidence and a masked example.
customers <- data.frame( cust_pan = c("ABCPE1234F", "AAACR5055K", "XYZPQ9876R"), notes = c("Called, Aadhaar 542229371182", "no issues", "mobile 9876543210") ) scan_pii(customers)customers <- data.frame( cust_pan = c("ABCPE1234F", "AAACR5055K", "XYZPQ9876R"), notes = c("Called, Aadhaar 542229371182", "no issues", "mobile 9876543210") ) scan_pii(customers)
Produces random identifiers that pass the same structural and checksum validation as real ones, for tests, demos and detector benchmarks. They are random, not taken from any real record. A random number with a valid checksum can still coincide with a real one by chance, so treat generated data as test data and never as anonymised data.
synth_indian_pii(n = 10, seed = NULL)synth_indian_pii(n = 10, seed = NULL)
n |
Number of rows. |
seed |
Optional random seed; the global RNG state is restored
afterwards.
Names are random combinations of common Indian first names and surnames;
email addresses use the reserved |
A data frame with columns name, name_hi (Devanagari),
aadhaar, aadhaar_vid, pan, gstin, card, voter_epic,
passport, driving_licence, mobile, email, upi_id, ifsc,
bank_account, abha_number, abha_address, pincode, address,
ckyc, ration_card, nrega_job_card, card_cvv and card_expiry.
synth_indian_pii(3, seed = 1)synth_indian_pii(3, seed = 1)
Replaces every detected identifier with a numbered token such as
[PAN_1] or [PERSON_NAME_2], and records in vault which value each
token stands for, so restore() can put the values back into a model's
answer. The same identifier gets the same token however it is written
(ABCPE1234F and abcpe1234f; +91 98765 43210 and 9876543210),
across texts and across calls with the same vault.
tokenise(text, vault, detectors = NULL, context = NULL, min_confidence = 0)tokenise(text, vault, detectors = NULL, context = NULL, min_confidence = 0)
text |
A character vector. |
vault |
A vault from |
detectors |
Detector ids to run; |
context |
Optional extra context per element of |
min_confidence |
Drop matches scoring below this value. |
A character vector like text, with identifiers replaced by
tokens. NA stays NA.
restore(), redact() for irreversible placeholders.
vault <- cg_vault() safe <- tokenise(c("PAN ABCPE1234F, mobile 9876543210", "pan abcpe1234f again"), vault) safe restore("The customer with [PAN_1] can be reached on [MOBILE_IN_1].", vault)vault <- cg_vault() safe <- tokenise(c("PAN ABCPE1234F, mobile 9876543210", "pan abcpe1234f again"), vault) safe restore("The customer with [PAN_1] can be reached on [MOBILE_IN_1].", vault)
Adds the entries of other that vault does not have yet, for example
from parallel workers that each tokenised part of a table with the same
key. Both vaults must be keyed with the same key; it is an error if a
token stands for different values in the two.
vault_merge(vault, other)vault_merge(vault, other)
vault, other
|
Keyed vaults from |
vault, modified in place, invisibly.
vault_save() writes the vault to a file encrypted with a passphrase, so
tokens can be restored in a later session - in R or in Python, which read
the same file. vault_open() reads it back; tokenising with the opened
vault continues its numbering.
vault_save(vault, path, passphrase = NULL) vault_open(path, passphrase = NULL, key = NULL)vault_save(vault, path, passphrase = NULL) vault_open(path, passphrase = NULL, key = NULL)
vault |
A vault from |
path |
File to write or read, conventionally ending in |
passphrase |
The passphrase, or |
key |
For a keyed vault: its token key, or |
The passphrase comes from passphrase, else the environment variable
CREDGATE_VAULT_PASSPHRASE, else a prompt (in an interactive session with
the askpass package). It needs at least 12 characters and is never
stored: without it the file cannot be opened. The file is written readable
only by you; an existing file is replaced only if it is itself a vault
file. Encryption uses libsodium (scrypt key derivation, XSalsa20-Poly1305)
through the sodium package.
vault_save() returns path invisibly; vault_open() returns a
vault.
vault <- cg_vault() tokenise("PAN ABCPE1234F", vault) path <- tempfile(fileext = ".cgvault") vault_save(vault, path, passphrase = "an example passphrase") # later, possibly in another session or in Python: vault <- vault_open(path, passphrase = "an example passphrase") restore("Customer [PAN_1] called.", vault)vault <- cg_vault() tokenise("PAN ABCPE1234F", vault) path <- tempfile(fileext = ".cgvault") vault_save(vault, path, passphrase = "an example passphrase") # later, possibly in another session or in Python: vault <- vault_open(path, passphrase = "an example passphrase") restore("Customer [PAN_1] called.", vault)
verhoeff_valid() checks whether a number (including its final check
digit) passes the Verhoeff checksum. verhoeff_check_digit() computes the
check digit to append to a number.
verhoeff_valid(x) verhoeff_check_digit(x)verhoeff_valid(x) verhoeff_check_digit(x)
x |
A character vector of digit strings. Spaces and hyphens are ignored. |
verhoeff_valid(): a logical vector (FALSE for anything that is
not all digits). verhoeff_check_digit(): a character vector of single
digits (NA for invalid input).
verhoeff_check_digit("236") verhoeff_valid("2363")verhoeff_check_digit("236") verhoeff_valid("2363")
write_inventory() saves an inventory from pii_inventory() as a CSV
file, written exactly as the spec describes, so that R and Python write
identical files for the same inventory. read_inventory() reads a file
written by either and checks its format.
write_inventory(x, path) read_inventory(path)write_inventory(x, path) read_inventory(path)
x |
An inventory from |
path |
File path. |
write_inventory() returns path invisibly; read_inventory()
an inventory.