Skip to content

Detectors

Section 12 · From docs/MCP_PRIVACY.md on GitHub

The detection model follows Microsoft Presidio and Google Cloud DLP: a detector only reports spans of an entity type with a confidence; the strategy is the redactor's business. Detectors are regular expressions with an optional validator; each also carries lower-case "hint" substrings, and its regex only runs on strings that contain one of them. Matches are taken leftmost first and a match that overlaps an earlier one is discarded, so an ARN swallows the account number inside it; when two matches start at the same position, the detector listed first wins.

12.1 Content detectors

The first detector in the list is cloudg_uri (category identifier, confidence 0.99). It matches cloudg's own resource URIs that name an asset or a dataset (cloudg://assets/web-1/neighbors, cloudg://datasets/prod/summary) and pseudonymises only the reference inside, so a URI in a result points at the same pseudonym as the asset's id. cloudg://findings/... and other URIs are not matched.

Secrets (category secret):

Detector Entity Conf. Matches Example that matches Example that does not
private_key private_key 1.0 PEM, OpenSSH and PGP private key blocks, to the END line or end of text -----BEGIN OPENSSH PRIVATE KEY-----... a PUBLIC KEY block
jwt jwt 0.95 eyJ<header>.eyJ<payload>.<signature> a complete JWT eyJhbGciOiJIUzI1NiJ9 alone
url_credentials password 0.95 The password in scheme://user:password@host (value only); placeholder check redis://default:pa55word99@cache.internal reports pa55word99 https://alice@git.example.com (no password; see url_userinfo)
connection_string_secret password 0.95 Value of Password=, Pwd=, AccountKey=, SharedAccessKey=, SharedSecret=, ClientSecret=; placeholder check ...;Password=S3cr3t!x; reports S3cr3t!x Password=;
aws_secret_access_key aws_secret_access_key 0.95 40 base64 characters within 20 characters of an aws ... secret or sk label aws_secret_access_key = wJalrXUt... the same 40 characters with no label (the high-entropy detector may still catch it)
secret_assignment secret_value 0.85 Value after password, secret, client_secret, api_key, access_token, token, bearer, private_key, passphrase... followed by : or =; at least 4 characters; placeholder check {"client_secret": "zq81Lm2Xp0"}, api_key: zq81Lm2Xp0 password = null, token: ${TOKEN}, api_key="<redacted>"
authorization_header secret_value 0.9 Credential after Bearer or Basic, 16+ characters Authorization: Bearer abcdefghijklmnop1234 Bearer abc123
github_token api_token 0.99 ghp_, gho_, ghu_, ghs_, ghr_ plus 36+, github_pat_ plus 22+ ghp_ followed by 36 characters ghp_short123
slack_token api_token 0.99 xoxa-, xoxb-, xoxp-, xoxo-, xoxs-, xoxr- plus 10+ xoxb-1234567890-abcdefghij xoxb-123
google_api_key api_token 0.99 AIza plus 35 AIza plus 35 characters AIzaShort
stripe_key api_token 0.99 sk_, rk_, pk_ + live_ or test_ + 16+ sk_live_x1x1x1x1x1x1x1x1x1x1 sk_live_short
azure_sas_signature secret_value 0.95 The sig= value of a SAS URL, 20+ characters ...&sig=AbCdEf0123456789%2BxyzQQ sig=short
azure_storage_key secret_value 0.8 Exactly 86 base64 characters plus == an 88-character storage key 85 characters plus ==
high_entropy_secret secret_value 0.5 Opaque tokens of 32+ characters: mixed case and digits, not hex, fewer than three 4-letter word runs, at least 4 bits of entropy per character Qm9vb3RzdHJhcEtleTEyMzQ1Njc4OTBhYmNkZWZn 40 hex digits, Zx9 repeated 12 times, my_password_is_long_and_wordy_value_here_ok

The placeholder check rejects empty values, null, none, true, false, undefined, redacted, n/a, empty, string, required, optional, runs of *, x, #, - or ., and anything starting with [redacted, ${, {{ or <.

Credentials and identifiers:

Detector Entity Category Conf. Matches Matches Does not match
aws_access_key_id aws_access_key_id credential 0.99 AKIA or ASIA plus 16 upper-case letters or digits AKIAIOSFODNN7EXAMPLE AKIA1234567890ABCDE (15)
aws_unique_id aws_unique_id identifier 0.95 AIDA, AROA, AGPA, AIPA, ANPA, ANVA, APKA, ABIA, ACCA plus 16 or 17 AROAJ2UCCR6DPCEXAMPLE AIDA123456789012345 (15)
aws_arn aws_arn identifier 0.99 arn:<aws partition>:<service>:<region>:<12 digits, aws or empty>:<resource> arn:aws:s3:::my-bucket, arn:aws-us-gov:... arn:partition:ec2:x
azure_resource_id azure_resource_id identifier 0.99 /subscriptions/<GUID> and the path after it /subscriptions/1b2c.../resourceGroups/prod-rg /subscriptions/not-a-guid/...
gcp_resource_name gcp_resource_name identifier 0.95 //<svc>.googleapis.com/... and relative projects/, organizations/, folders/ names projects/acme-prod-1/topics/orders projects/ab/x (id too short), projects-list/zones
azure_subscription_ref azure_subscription_id identifier 0.9 A GUID after subscription id or tenant id and : or = tenant_id: 9f86d081-... a bare GUID
url_userinfo url_username pii 0.9 The user name in scheme://user@host when there is no password https://alice@git.example.com/repo reports alice (pseudonym user-...) https://git.example.com/repo
email email pii 0.95 local@domain.tld alice@example.com user@localhost, the user info of a URL
aws_account_id aws_account_id identifier 0.7 A bare 12-digit number not inside a longer token; all-same-digit placeholders rejected account 123456789012 1234567890123, 000000000000, /aws/lambda/fn-123456789012, 123456789012.5
uuid uuid identifier 0.6 Any GUID 9f86d081-884c-4d63-9a2b-1c5e3f0a7b21

Network (category network):

Detector Entity Conf. Matches Matches Does not match
ipv6 refined (below) 0.9 IPv6 addresses and CIDRs with real structure, validated with ipaddress fe80::1, 2600:1f18::/32, ::1, ::/0 std::vector, a::b, ::
ipv4 refined (below) 0.9 Dotted quads and CIDRs, validated 10.0.1.5, 54.12.33.4/32, 0.0.0.0/0 999.1.1.1, 10.0.0.256, 1.2.3.4.5
mac_address mac_address 0.8 Six hex pairs separated by : or - 00:1a:2b:3c:4d:5e five pairs
hostname hostname 0.7 Fully qualified names ending in a common TLD or internal, local, localdomain, lan, corp, intra db1.prod.corp.com, ip-10-0-1-5.ec2.internal, my-bucket.s3.amazonaws.com config.yaml, service.example.invalid

Temporal: timestamp (category temporal, 0.9) matches ISO 8601 date-times such as 2024-05-01T12:30:00Z, not bare dates.

In https://alice@git.example.com/repo, url_userinfo reports alice and hostname reports git.example.com; the email detector does not fire.

12.2 IP classification

The IP detectors refine their entity with classify_ip_text:

Class Addresses Entity
special unspecified (0.0.0.0, ::), loopback, link-local (including the 169.254.169.254 metadata endpoint and fe80::/10), multicast, IPv4 reserved (240.0.0.0/4), 255.255.255.255; any /0 route special_ip, special_cidr
private 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 100.64.0.0/10 (CGNAT), IPv6 ULA fc00::/7 private_ip, private_cidr
public everything else, including the documentation ranges public_ip, public_cidr

A CIDR is classified by its network address. This is why strict can say special_ip: keep and special_cidr: keep: "SSH open to 0.0.0.0/0" and "IMDS at 169.254.169.254" stay readable, and they identify nobody.

12.3 Key rules

Keys are normalised before matching: camelCase is split and every run of non-alphanumerics becomes _, so SecretAccessKey, secret-access-key and secretAccessKey all become secret_access_key.

Rule Entity Category Applies to keys Extra
sensitive_key sensitive_field secret Containing a word such as password, passwd, pwd, passphrase, secret(s), client_secret, api_key, access_key, secret_key, private_key, token(s), access_token, refresh_token, id_token, session_token, auth_token, bearer, credential(s), authorization, auth, cookie(s), connection_string, conn_str, sas_token, sas, signature, user_data, userdata, primary_key, secondary_key, account_key, shared_key, master_key, admin_password, kubeconfig, pem Whole subtree; numbers too; safe-key exclusions below
account_id_key cloud_account (typed by shape, section 11.2) identifier account_id, owner_id, account, account_ids, accounts, each optionally prefixed aws_ or cloud_ defers to detectors
subscription_key azure_subscription_id identifier subscription, subscription_id, azure_subscription_id defers
tenant_key azure_tenant_id identifier tenant, tenant_id, azure_tenant_id defers
gcp_project_key gcp_project_id identifier project, project_id, gcp_project_id defers
gcp_project_number_key gcp_project_number identifier project_number numbers too; defers
resource_name_key resource_name identifier resource_name, display_name, bucket_name, function_name, instance_name, cluster_name, db_name, database_name, computer_name, vm_name, role_name, user_name, username, group_name, key_name, table_name, queue_name, topic_name, repository_name, asset_name, source_name, target_name, resource_names, asset_names defers
resource_ref_key resource_name identifier ref, refs, asset_ref(s), asset_id(s), resource_id(s), resource, asset, source, target, source_id, target_id, source_ref, target_ref, node, node_id, nodes, from, to, start, end, via, members, neighbors, neighbours, path defers; lists of references too
dataset_key dataset_name identifier dataset, dataset_name, datasets, active_dataset, base, base_dataset, target_dataset defers; pseudonym ds-...
dataset_listing_name_key dataset_name identifier name Only in a dict that also has loaded_at or compliance_results (dataset listings); defers
asset_name_key resource_name identifier name Only in a dict that also has asset_type, arn, provider, resource_type, account_id or region, so tool and prompt names in catalog listings and policy descriptions are left alone; defers
asset_id_key resource_name identifier id Same sibling condition as asset_name_key; defers
hostname_key hostname network hostname, host_name, dns_name, fqdn, private_dns_name, public_dns_name, domain_name, endpoint_address defers

A key rule only does something when its strategy is not keep, so under standard only sensitive_key is active. "Defers" means the content detectors decide first when one of them matches the whole value (section 11.2). Custom key rules (extra_key_rules) can use requires_sibling, but defer cannot be set from configuration, so a custom rule always applies its own entity.

Safe-key exclusions stop sensitive_key from firing on keys that describe a secret instead of holding one. A key is excluded if it ends in _id, _ids, _arn, _name, _type, _state, _status, _usage, _enabled, _last_used, _last_used_date, _last_changed, _last_rotated, _age, _length, _count, _policy, _rotation..., _expir..., _created..., _date, _time, _version, _present, _required, _set, _configured, _manager, _source, _format, _algorithm, _kind, _ref, _location, _uri, _url, _endpoint, _header, _scheme, _method, _mode, _provider, _issuer, _audience, _ttl, _lifetime, _reset_required, _exists, _level, _size, _fingerprint, _thumbprint, _hint (and a few plurals), or starts with has_, is_, num_, require(s)_, allow(s)_, enable(d)_, use(s)_, max_, min_. So password_last_used, kms_key_id, has_password and token_expiry are left alone, while password, db_password and client_secret are replaced.

12.4 Inspecting detectors

list_detectors (and cloudg://privacy/detectors) shows each detector, the strategy the caller's generic pipeline gives it, the strategies of its refined entities and whether it is active. Under strict with category: network, abridged:

{
  "policy": "strict",
  "detectors": [
    {"name": "ipv4", "entity": "ip_address", "category": "network", "confidence": 0.9,
     "strategy": "pseudonymize",
     "refined": {"private_ip": "pseudonymize", "public_ip": "pseudonymize", "special_ip": "keep"},
     "active": true},
    {"name": "hostname", "entity": "hostname", "category": "network", "confidence": 0.7,
     "strategy": "pseudonymize", "active": true}
  ],
  "key_rules": [
    {"name": "hostname_key", "entity": "hostname", "category": "network",
     "key_pattern": "^(?:hostname|host_name|dns_name|fqdn|private_dns_name|public_dns_name|domain_name|endpoint_address)$",
     "strategy": "pseudonymize"}
  ]
}

12.5 Custom detectors

Declare detectors in the policy; they are added to every redactor and secret guard the policy builds.

detectors:
  - name: jira_ticket            # required, unique; replaces a built-in of the same name
    entity: ticket_id            # defaults to the name
    category: internal           # defaults to identifier
    pattern: '\b(?:SECOPS|INFRA)-\d{2,6}\b'   # required; numbered groups only
    group: 0                     # which group is the reported span (0 = whole match)
    confidence: 0.9              # default 0.8
    hints: ["secops-", "infra-"] # lower-case pre-filter substrings; empty = always run
    flags: [ignorecase]          # i, m, s, x or their long names; or ignore_case: true
    validator: null              # ip | secret | not_placeholder | entropy[:bits] | high_entropy[:bits]
    min_length: 1
    enabled: true
transform_options:
  redact:
    strategies:
      ticket_id: hash

Bad patterns fail when the policy loads. Without hints the regex runs on every string, which you will notice on large results. The validator names mean: ip validates and refines like the IP detectors, secret and not_placeholder apply the placeholder check, entropy:N requires N bits per character, and high_entropy:N applies the full opaque-token heuristic.

disabled_detectors: [email, hostname] turns built-ins off for the whole policy. Section 17.5 has a measured example.