diff --git a/CLAUDE.md b/CLAUDE.md index db0ed47..72ae4f2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -24,6 +24,6 @@ ECS-compliant structured logging with W3C Trace Context support. Framework-agnos - Django's `LogContextBinder` must be imported explicitly (not in `__init__.py`) to avoid circular imports - `LoggingContextMiddleware` must be placed AFTER auth middleware to capture user_id - CVV never carries a token, even when PII is configured — PCI forbids storing CVV in any form, so `[CVV-MASKED]` is always the literal, final output -- Card number **content** rules (and the bare 3-4 digit CVV rule, which needs card context) are the opt-in `pci` pack (`get_logging_config(masking_packs=("pci", "financial_ids"))` or `ECSCTX_MASKING_PACKS`): a PCI service that does not enable it gets no PAN content scanning. The keyed CVV text rules (`cvv=123`, `"securityCode": "123"`, `CVV 123`) are `default`: a CVV must not ship from any service, and Connect (default pack) receives the CVV a saved-card payment sends. Key names (`card`, `pan`, `cvv`, `expiry`, …) are masked in every service +- Card number **content** rules (and the bare 3-4 digit CVV rules, which read only a CVV beside a card number or its truncation, or after a CVV word) are the opt-in `pci` pack (`get_logging_config(masking_packs=("pci", "financial_ids"))` or `ECSCTX_MASKING_PACKS`): a PCI service that does not enable it gets no PAN content scanning. The keyed CVV text rules (`cvv=123`, `"securityCode": "123"`, `CVV 123`) are `default`: a CVV must not ship from any service, and Connect (default pack) receives the CVV a saved-card payment sends. Key names (`card`, `pan`, `cvv`, …) are masked in every service; expiry keys are safe keys, never masked. A value a service's value rule matches (`ECSCTX_MASK_VALUE_RULES`, found by shape) is its label in every pack; core names no shape itself -- Ottu's wallet tokens are `ecsctx.contrib.ottu.masking.WALLET_RULES`, each service's to list - Card numbers truncate to first 6 + last 4 for 15–19 digits and last 4 only below 15 (`411111******1111`, bare, PCI DSS 3.5.1, FAQ 1091) — no token alongside, independent of PII configuration, and a second masking pass is a noop - `install_maskers()` only sweeps handlers that exist at call time (no stdlib patching) — a handler built after the call needs another call diff --git a/README.md b/README.md index 90b33b0..6ba001e 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,8 @@ Framework-agnostic core with Django, Celery, and RQ integrations. **Key takeaway**: Your app writes JSON to stdout. Vector picks it up, ships it to Elasticsearch. The field structure of that JSON determines whether it's searchable in Kibana or silently dropped due to mapping conflicts. That's why ECS compliance matters. +> **At Ottu** the source and the destination differ; the JSON is the same. Services write their JSON lines to log files (a rotating file handler) that Vector tails, and ship to daily `logs-YYYY.MM.DD` indices; a PCI service (Ottu PG) ships to `logs-pci-`. The docker-and-data-stream setup here is the generic one. + --- ## 3. Architecture: Request Flow @@ -101,12 +103,12 @@ Framework-agnostic core with Django, Celery, and RQ integrations. ↓ 6. Your middleware/views bind domain context (merchant_id, session_id, etc.) ↓ -7. View executes, calls logger.info("event_name", field=value) +7. View executes, calls logger.info("payment created", field=value) ↓ 8. Processor chain: contextvars_injector → namespace_ecs_fields → mask_sensitive_data → ecs_validator ↓ -9. ECS-formatted JSON → stdout → Vector → Elasticsearch +9. ECS-formatted JSON → stdout (at Ottu, a log file) → Vector → Elasticsearch ``` ### Processor Chain (Execution Order) @@ -334,7 +336,7 @@ import structlog logger = structlog.get_logger(__name__) def my_view(request): - logger.info("payment_processed", amount=100, currency="KWD") + logger.info("payment processed", amount=100, currency="KWD") # Output includes: trace.id, span.id, user.id, client.ip, service.name, etc. ``` @@ -407,7 +409,7 @@ async def inject_logging_context(request: Request): @app.post("/payments", dependencies=[Depends(inject_logging_context)]) async def create_payment(): - logger.info("payment_created") + logger.info("payment created") ``` --- @@ -545,7 +547,7 @@ class WebhookView(APIView): } } ) - log.info("webhook_received") # Has: merchant_id + session_id + app-specific IDs + log.info("webhook received") # Has: merchant_id + session_id + app-specific IDs # Layer 3: Task — additional info discovered during processing @app.task @@ -553,7 +555,7 @@ def process_webhook(self, enterprise_id, store_id): # Context from view is auto-propagated (Celery hooks) merchant = Merchant.objects.filter(...).first() bind_logging_context(extra={"merchant_id": merchant.name}) # NEW info - log.info("task_started") # Has everything from view + merchant_id + log.info("task started") # Has everything from view + merchant_id ``` ### Two Binding Mechanisms @@ -566,8 +568,8 @@ bind_logging_context(session_id="abc123", extra={"merchant_id": "acme"}) # 2. Context manager — auto-restores previous context on exit (scoped) with logging_context(session_id="abc123"): - log.info("scoped_event") # has session_id -log.info("outer_event") # session_id gone + log.info("scoped event") # has session_id +log.info("outer event") # session_id gone ``` ### The `extra` Parameter @@ -599,12 +601,12 @@ bind_logging_context(extra={"myapp": {"store_id": "s1"}}) ```python # WRONG — first log has no context -log.info("webhook_received") +log.info("webhook received") bind_logging_context(session_id=session_id) # CORRECT — bind first, then log bind_logging_context(session_id=session_id) -log.info("webhook_received") +log.info("webhook received") ``` ### Don't Re-state Context in Log Calls @@ -615,10 +617,10 @@ If a field is already bound, don't pass it again: bind_logging_context(extra={"merchant_id": "acme"}) # WRONG — merchant_id already in context, this is redundant noise -log.info("payment_created", merchant_id="acme") +log.info("payment created", merchant_id="acme") # CORRECT — it's already there -log.info("payment_created") +log.info("payment created") ``` --- @@ -652,7 +654,7 @@ See the [Core Rules](#4-core-rules-field-placement-reference) table for the comp ```python # Use ** unpacking when the namespace key is a variable -log.info("event_started", **{ +log.info("event started", **{ settings.SHOPIFY_APP_NAME: { "shop": shop_domain, "reference": reference, @@ -699,12 +701,12 @@ def process_webhook(self, enterprise_id, store_id): merchant = Merchant.objects.filter(...).first() if not merchant: - log.info("merchant_not_found") # App namespace IDs come from context + log.info("merchant not found") # App namespace IDs come from context self.retry(countdown=30) # ✅ Bind merchant_id AFTER lookup — this is NEW info the view didn't have bind_logging_context(extra={"merchant_id": merchant.name}) - log.info("task_started") + log.info("task started") ``` ### Beat-Dispatched Tasks: Start from ZERO @@ -716,7 +718,7 @@ Celery Beat has no `LoggingContext` to propagate. **You MUST bind everything at def process_payment_inquiry(self, merchant_id, session_id): # ✅ Beat task — MUST bind everything, nothing is propagated bind_logging_context(session_id=session_id, extra={"merchant_id": merchant_id}) - log.info("inquiry_started") + log.info("inquiry started") ``` ### Quick Reference @@ -740,7 +742,7 @@ from ecsctx.contrib.rq import with_log_context @with_log_context def my_background_task(user_id, amount): - logger.info("processing_payment") # Automatically has request context + logger.info("processing payment") # Automatically has request context ``` ### Manual Context Capture (Custom Enqueue) @@ -827,17 +829,22 @@ strings already known clean are not scanned twice. A record that would not format once masked — a %-style template masking itself changes (in `password=%s` the credential rule reads the placeholder as the credential), a lazy translation or an exception as the message, a number masking turned into -text under `%d`, arguments that never fit — is rendered from the masked -arguments and masked whole, or is `[MASKING-FAILED: …]`: no line is dropped, -and no argument reaches stderr. The one exception is a dict or list message -with arguments, which logging cannot format at all: as in stdlib, the line is -dropped and `handleError` prints the arguments, masked. A template masking -leaves as it is, and that formats, keeps its arguments. +text under `%d` (every conversion CPython's `%` reads, `%.f` included), arguments +that never fit — is rendered from the masked arguments and masked whole, or is +`[MASKING-FAILED: …]`: no line is dropped, and no argument reaches stderr. So +is a record whose line masks differently from its masked arguments: a value +split between the template and an argument (`logger.info("cvv=%s", "123")`) +renders `cvv=[CVV-MASKED]`, for the formatter and for everything that reads the +record. The one exception is a dict or list message with arguments, which +logging cannot format at all: as in stdlib, the line is dropped and +`handleError` prints the arguments, masked. A record whose template masking +leaves as it is, and whose masked arguments render a line with nothing more to +mask, keeps its arguments. **Log processor path** (automatic via `mask_sensitive_data`): - When PII is configured (`PII_PROVIDER=file|vault`): detected values become deterministic **HMAC-SHA-256** tokens (`ptok:v1:...`), for fraud correlation. Same input always produces the same token. Where no token can be made (PII not configured, or tokenization failing) the value becomes its type's label, `[EMAIL-MASKED]`; CVV is never tokenized and carries nothing, so it keeps a bracketed label (`[CVV-MASKED]`); a card number is never tokenized either, but its truncation IS carried, so it is bare (`411111******1111`) — under a card key, and with the `pci` pack under any key, including a name or email field. Expiry is not masked at all. A null stays null, and an empty value stays empty. - When PII is not configured: detected values become the bare label (`[EMAIL-MASKED]`) — raw PII never appears in logs. -- Cardholder data is never tokenized: PANs are truncated to `411111******1111` whatever key they sit under — including, with the `pci` pack (the filter's own or the process's), a name or email field, because a keyed hash beside a truncation of the same PAN is the correlation PCI DSS FAQ 1117 warns about. With `pci`, a value that a PII, secret or id key would tokenize becomes its label instead when it holds a run of 12 or more digits among other text: a PAN typed beside a name, and also a long reference number or hex id, since the check fails closed. A value that is a canonical UUID (8-4-4-4-12 hex digits with a hex letter) holds none and keeps its token. In a phone field, a number written after `+` with at most 15 digits (E.164) is the phone number and keeps its token. CVV is always `[CVV-MASKED]`. **Expiry is not masked**: it is Cardholder Data rather than Sensitive Authentication Data, so PCI permits storing it, and masking it only cost the ability to read an expired-card decline. The rule to remember: **brackets mean nothing survived**. +- Cardholder data is never tokenized: PANs are truncated to `411111******1111` whatever key they sit under — including, with the `pci` pack (the filter's own or the process's), a name or email field, because a keyed hash beside a truncation of the same PAN is the correlation PCI DSS FAQ 1117 warns about. With `pci`, a value that a PII, secret or id key would tokenize becomes its label instead when it holds a run of 12 or more digits among other text: a PAN typed beside a name, and also a long reference number or hex id, since the check fails closed. A value that is a canonical UUID (8-4-4-4-12 hex digits with a hex letter) holds none and keeps its token. In a phone field, a number written after `+` with at most 15 digits (E.164) is the phone number and keeps its token. An email or phone number found in text is judged the same way: with `pci`, `4111111111111111@example.com` is `[EMAIL-MASKED]` and `+4111111111111111` `[PHONE-MASKED]`, not a keyed hash. CVV is always `[CVV-MASKED]`. **Expiry is not masked**: it is Cardholder Data rather than Sensitive Authentication Data, so PCI permits storing it, and masking it only cost the ability to read an expired-card decline. The rule to remember: **brackets mean nothing survived**. **Explicit encryption API** (standalone, NOT part of the log processor pipeline): - `protect()` / `reveal()` use **AES-256-GCM** for randomized ciphertext (`penc:v1::...`) when reversible encryption is needed. Requires `PII_ACCESS=full`. @@ -856,8 +863,8 @@ receives the CVV a saved-card payment sends, so the keyed CVV rules are | Pack | Content rules | On by default | |------|---------------|---------------| -| `default` | PEM keys, credentials (`token=…`, `"secret": …`, `Bearer …`), keyed CVV (`cvv=123`, `"securityCode": "123"`, `CVV 123`, `vpc_CardSecurityCode=123`, `paymentCvv=123`), a URL's userinfo, phone numbers, emails, JWTs | always | -| `pci` | PANs (truncated), bare 3–4 digit CVV groups beside card context | no | +| `default` | PEM keys, a JSON object a service's [value rule](#value-rules-a-services-own-shapes) matches (its label), credentials (`token=…`, `"secret": …`, `Bearer …`, `API-Key …`, `…`, `password=['a', 'b']`), keyed CVV (`cvv=123`, `"securityCode": "123"`, `CVV 123`, `vpc_CardSecurityCode=123`, `paymentCvv=123`), an XML element named as a card, CVV or SAD key (`123`, `1234`, `…` truncated), a URL's userinfo, phone numbers, emails, JWTs | always | +| `pci` | PANs (truncated), a bare 3–4 digit CVV beside a card number or its truncation (`4111111111111111 123`, `411111******1111\|12/27\|123`) or after a CVV word (`the cvv is 123`) | no | | `financial_ids` | IBANs, SSNs, payment/transaction/auth ids (content and key names) | no | A PCI-scoped service enables them in its logging config: @@ -874,9 +881,20 @@ with a warning, and the boot check reports it. Upgrading from 0.7.x without opting in turns PAN content scanning (and the bare-digit CVV rule) **off**. +The bare-digit CVV rule reads only the fields written after a card number or +its truncation — up to three, joined by whitespace, `|`, `,`, `;` or `:` — and +masks each that is three or four digits alone: which of `1227` and `123` after +a card is its CVV no rule can tell, so neither ships. A date (`12/27`), a field +after an expiry word (`exp 1227`), a month or year of one or two digits, and +digits that run on into a date, a time or an amount (`2026-09-30`, `10:30`, +`100.000`) stay. Before 0.15.6 it masked any 3–4 digit group in a line that +mentioned a card: `card list returned 200 in 350 ms` lost its status and its +duration, `declined with code 051` its code. + Key names are checked in every service regardless of packs: a key named -`card`, `pan`, `card_number`, `cvv`, `securityCode`, `expiry`, `exp_month`, … -masks its value wherever it appears. +`card`, `pan`, `card_number`, `cvv`, `securityCode`, … masks its value wherever +it appears, and so does an XML element of that name in text. Expiry keys (`expiry`, `exp_month`, `expiry_year`, …) do not: they +are safe keys, and read through. ### PAN truncation (`mask_pan`) @@ -915,7 +933,8 @@ builds itself — is masked as the key it would sit under masks it: record that carries it, for a value in a token's exact shape with a card number in it (`ptok:v1:` typed before one), and for a card number's truncation (`411111******1111`: ten digits of a saved card's gateway - token) — each judged as written and as a + token), after an auth scheme too (`Bearer 411111******1111`: never a token + of the scheme and the truncation) — each judged as written and as a URL or form encoding decodes it (`%224111…%22`, `4111+1111+…`). With `pci` among the call's packs, and the process's — a `MaskPIIFilter`'s own `packs=` while it masks, with the process's — it is `[SECRET-MASKED]` too @@ -958,17 +977,44 @@ from ecsctx.contrib.net import ( literal secret (`ecs_url(redact_url(full_url, secrets=[token]))` — masking twice masks once). - `redact_body(text)` — masks credential values (`access_token`, - `client_secret`, …) in JSON and form-encoded bodies. A value is masked as - what it decodes to (a JSON escape, a form encoding), and one already masked - passes through. A form value runs to the next `&` or whitespace; the - quotes and closing brackets at its ends stay as written (``). A value, JSON or form, holding a card-number - run is `[SECRET-MASKED]` either way. A form field whose key the key rules - call a card, CVV or other SAD is masked by that type, as in a URL - (`cvv=[CVV-MASKED]&card_number=411111******1111`). A bare `token` key is - masked as the key walk masks it: a saved card's sixteen-digit gateway token - is `[SECRET-MASKED]`, and any other value its token (with a keyset, so a - gateway's payment or session id still correlates) or label. + `client_secret`, …) in JSON, form-encoded and XML bodies. A value is masked + as what it decodes to (a JSON escape, a form encoding, XML entities or a + CDATA section), and one already masked passes through. A form value runs to + the next `&`, whitespace or end tag; the quotes and closing brackets at its + ends stay as written (``). A value, JSON, + form or XML, holding a card-number run is `[SECRET-MASKED]` either way. A + form field whose key the key rules call a card, CVV or other SAD is masked + by that type, as in a URL (`cvv=[CVV-MASKED]&card_number=411111******1111`). + A bare `token` key is masked as the key walk masks it: a saved card's + sixteen-digit gateway token is `[SECRET-MASKED]`, and any other value its + token (with a keyset, so a gateway's payment or session id still + correlates) or label. + + An XML element is read as a key (KNET's KPay takes XML): its local name, + whatever its namespace prefix and attributes, case-insensitively. A card, + CVV or SAD name masks its text by that type, as the text rules do + (`[CVV-MASKED]`, `411111******1111`), + and a card element holding elements — a card object, CyberSource's + `` — is read field by field: a CVV or SAD field is its label and + any other field's text a card value, so `` is truncated + and an expiry reads through. Then a credential name — one `redact_url` + reads as a credential param (`password`, `pwd`, `auth`, `user`, `sign`, + `key`, …) or one of the secret keys — masks its text as a credential + (`ptok:v1:…`), to its end tag, children and all, a + CVV inside it already its label. So ``, ``, `` and + `` are credentials, as the same names are in a URL's query: one + reading of a name across a URL and a body, rather than an exception for + one gateway's elements. A name is matched by substring, as a param's is, + so ``, ``, ``, `` and `` are + masked as credentials too: the accepted cost of failing closed. A start tag with no end tag is no element: a + route (`/v1/cards//`) has that shape. JSON or XML in an + element's text, written with entities, goes through these rules as it + decodes and is written back escaped. + + A value a service's [value rule](#value-rules-a-services-own-shapes) + matches is its label before anything else reads it, as JSON anywhere in + the body — an XML element's text included — or as JSON in a JSON + string. - `redact_url(url, secrets=[token])` also masks literal values anywhere in the URL, longest first — a saved-card token in a path such as `/card//`. @@ -995,13 +1041,10 @@ it `mask_secret` (and so `redact_url`) hashes such a value. Before 0.15.4 they wrote a fixed `[REDACTED]`, whatever the keyset. -Accepted residual: with a keyset, the credential text rules followed by -`redact_body` are not a fixed point where `redact_body` hashes a form value -the text rules left, right after a scheme word (`Bearer password=[SECRET-MASKED]"x`, -`Bearer password='`). The scheme word's rule then reads `password=` -as its own credential and hashes it again: a token of a token, which affects -correlation only. Skipping `key=` there would let a credential glued -to a key name through (`Token abc123abc123api_key=…`). +The credential text rules followed by `redact_body` are a fixed point: after a +scheme word, a form value `redact_body` masked (`Bearer password=`) is +no longer read as the scheme's credential and hashed again, while a credential +glued to a key name (`Token abc123abc123api_key=…`) still is (0.15.6). Configure per deploy without code changes. Precedence: explicit call > Django settings > env vars > defaults (same lazy pattern as the masking @@ -1063,16 +1106,18 @@ ECSCTX_REDACT_BODY_LOG_CAP=8192 A key name marks its value when the lowercased key **contains** a keyword, so glued and plural names payloads use (`phonenumber`, `cardcvv`, `nameoncard`, `tokens`) are caught; the known false positives are listed below as safe keys. -Card and expiry keys are matched precisely. +Card keys are matched precisely. Expiry keys are not masked at all: they are +safe keys (the expiry spellings in the whitelist below). | Type | Key names | Content rule (pack) | Output | |------|-----------|---------------------|--------| | **Secrets** | ending in `token`, `secret`, `password`, `passwd`, `passphrase`, `passcode`, `pwd`; `authorization` (also `HTTP_AUTHORIZATION`, `Proxy-Authorization`), `cookie`, `bearer`, `basic`, `digest`, `credential(s)`, an `api`/`access`/`secret`/`private`/`hmac`/`merchant`/… `_key(s)`, `access_code` | credential forms (`default`) | `[SECRET-MASKED…]`; always the label for a PAN-shaped credential (never truncated, never hashed), for a placeholder another masker left (`[REDACTED]`, `***`) and for a token-shaped value with a card number in it; with `pci`, also for a credential or payment id that holds a card-number run anywhere | -| **Emails / phones** | containing `email`; `phone`, `mobile`, `tel` | `default` | `[EMAIL-MASKED…]`, `[PHONE-MASKED…]` | +| **Emails / phones** | containing `email`; `phone`, `mobile`, `tel` | `default` | `[EMAIL-MASKED…]`, `[PHONE-MASKED…]`; with `pci`, the label for one that holds a card-number run (a phone number after `+` up to E.164's 15 digits keeps its token) | | **Names / addresses / other PII** | containing `name`, `cardholder`, `payer`, `beneficiary`, `recipient`; `card_details` (the whole key); `address`; `billing`, `shipping`, `customer`, `contact`, `udf` | — | `[NAME-MASKED…]`, … | -| **PANs** | `card`, `pan`, `card_number`, `cardNumber`, `card_no`, `card_num` (MIGS's `vpc_CardNum`) | 12–19 digit runs (`pci`) | `411111******1111` | -| **CVV** | containing `cvv`, `cvc`, `security code`, `verification value`, or the words `csc`, `cvd`, `cvn`, `card code` — unless what follows names something *about* one (`cvv_required`, `cvvResult`, `cardSecurityCodeError`) | keyed CVV (`default`), a bare 3–4 digit group beside card context (`pci`) | `[CVV-MASKED]` | -| **SAD** | track data (`track2`, `trackData`, `raw_track`; not `track_id`), `pin`/`pinBlock`, EMV/chip data, and ending in `cryptogram`, `cavv`, `tavv`, `aav`, `ucaf` | — | `[SAD-MASKED]` | +| **PANs** | `card`, `pan`, `card_number`, `cardNumber`, `card_no`, `card_num` (MIGS's `vpc_CardNum`) | 12–19 digit runs (`pci`); an XML element so named (`default`) | `411111******1111` | +| **CVV** | containing `cvv`, `cvc`, `security code`, `verification value`, or the words `csc`, `cvd`, `cvn`, `card code` — unless what follows names something *about* one (`cvv_required`, `cvvResult`, `cardSecurityCodeError`) | keyed CVV and an XML element so named (`default`), a bare 3–4 digit group beside a card number or its truncation, or after a CVV word (`pci`) | `[CVV-MASKED]` | +| **SAD** | track data (`track2`, `trackData`, `raw_track`; not `track_id`), `pin`/`pinBlock`, EMV/chip data, and ending in `cryptogram`, `cavv`, `tavv`, `aav`, `ucaf` | an XML element so named (`default`) | `[SAD-MASKED]` | +| **A service's value rules** | none: found by shape under any key, as a mapping or as JSON text ([value rules](#value-rules-a-services-own-shapes)) | the same shapes as JSON in text, or in a JSON string (`default`) | the rule's label, the whole value | | **National ids** | `civil_id`, `national_id`, `passport`, `iqama`, `qid`, `cpr`, `nid`, `emirates_id`, `ssn`, `tin`, `tax_id`, `aadhaar`, `id_number` | — | `[SSN-MASKED…]` | | **IBAN / SSN / payment ids** | `payment_id`, `transaction_id`, `auth_id` (`financial_ids`) | `financial_ids` | `[IBAN-MASKED…]`, … | @@ -1085,6 +1130,11 @@ and last four, expiry and scheme read through, and the CVV, track data and PIN are destroyed — collapsing it threw away the one form PCI DSS 3.5.1 permits us to keep. +A value a service's **value rule** matches is that rule's label whole, in +every pack and under any key, never hashed; it is found by shape, so anything +else under the same key reads as before. A value encoded into another field +(hex, base64) has no shape to find, and is left to that field's key. + Since 0.14.0 a **credential, CVV or SAD** key holding a container is walked as well, because it cannot be holding the value itself: a leaf with no rule of its own still takes the container's type, and under a CVV or SAD key only a key the @@ -1102,8 +1152,9 @@ A credential value found in text (`password=…`, `"token": "…"`, `Bearer …` runs to its delimiter, as a form value in `redact_body` does: an unquoted one to whitespace, `&`, `;`, `,`, a closing bracket or a closing quote (a quote followed by a JSON key's `":` or an element's `/>` is structure too); a quoted -one to its unescaped closing quote, spaces and all. An empty pair of quotes is -no value: doubled as CSV and SQL escape a quote (`password=""s3cret`), they +one to its unescaped closing quote, spaces and all. Neither runs into an end +tag: in `password=abc` the value is `abc`. An empty pair of quotes +is no value: doubled as CSV and SQL escape a quote (`password=""s3cret`), they are structure before it. A value between escaped quotes — JSON in a JSON string, `{\"password\": \"correct horse\"}` — runs to its matching escaped quote too. A backslash, `@`, `#` or an @@ -1119,16 +1170,45 @@ value — `Authorization: Bearer abc…` carries the token the header gets under key, not one for `Bearer` with the credential beside it. A whole token right after the scheme — what masking the credential alone leaves, `Authorization: Bearer ptok:v1:…` — is left as it is, not hashed again with its -scheme. A credential keyword that -names a CVV or PIN (`cvv_token=123`, `pin_password=1234`) gives that label -(`[CVV-MASKED]`, `[SAD-MASKED]`), as the key does, never a token. +scheme. Ottu's `API-Key` scheme word standing alone does the same, without its +`Authorization:` — `sending API-Key abc123XYZ to core` carries the header's +token, `mask_secret("API-Key abc123XYZ")` — for any key after it, digits or +none, however short: the word is the evidence (`Bearer` and the other schemes +keep their eight characters and a digit, since prose says "Bearer of bad +news"). A header name ending in the word (`X-API-Key `) is no scheme, and +its key alone is masked. A credential +keyword that names a CVV or PIN (`cvv_token=123`, `pin_password=1234`) gives +that label (`[CVV-MASKED]`, `[SAD-MASKED]`), as the key does, never a token. + +A container after a credential keyword — `password=%(pw)s` rendered with a list, +`password=['a', 'b']`, or a mapping's repr, `{'password': ('a', 'b')}` — is the +credential's value whole: its token, or the label (a repr's value kept a +string). Not a JSON key's container, which the key walk masked by its keys +before writing it, nor one holding only masking's own output. + +An XML element named by a credential keyword — `s3cret`, +`…`, `Bearer x` +— is masked to its end tag, as it decodes, in every pack, for a body that never +went through `redact_body`; a start tag with no end tag (a route, +`/v1/cards//`) is no element. So is an element named as a card, CVV +or SAD key, as `redact_body` masks it: `123` is +`[CVV-MASKED]`, `1234` `[SAD-MASKED]`, and +`` truncated — a test card that fails Luhn too, with or without +`pci` — and a card object is read field by field. A CVV inside a credential's +element is its label before the credential is masked, as under a credential +key. A JSON object a service's value rule matches, written in text or as +JSON in a JSON string, is the rule's label before any credential rule reads +it. A URL's userinfo in text — a DSN in an exception, `postgresql://user:password@db:5432/app` — is masked part by part as `redact_url` masks it, keeping the scheme, host and port: with a password, -empty or not (`https://key:@host`); a user alone (`ssh://git@host`) is left to -the other rules. The email rule never starts inside masking's own output, so a -token or label in a URL's userinfo keeps the host after it. +empty or not (`https://key:@host`). A user alone (`ssh://git@host`) is left to +the other rules, unless it is shaped like a card number or holds a card-number +run (`https://9923960000004314@host/`, a saved card's gateway token): that is +`[SECRET-MASKED]`, as `redact_url` gives it. The email rule never starts inside +masking's own output, so a token or label in a URL's userinfo keeps the host +after it. A digit run that touches a letter is never a phone number — it is part of an id. The card rule still matches a PAN followed by a letter, because Track 2 @@ -1241,19 +1321,79 @@ from ecsctx.masking import configure_masking_safe_keys configure_masking_safe_keys(["pg_name", "cvv_required"]) ``` -Names are matched case-insensitively. A card or expiry key, or a name ending in -a CVV or credential word (`card_number`, `pan_no`, `expiry_month`, `card_cvv`, -`db_password`, `oauth_token`, `api_key`, …) names the value itself and cannot be -listed; a flag or status about one (`cvv_required`, `tokenization_status`) can: `configure_masking_safe_keys` raises, and from the setting or -env var it is dropped with a warning, stays masked, and fails the Django boot -check. Ottu services use `ecsctx.contrib.ottu.masking.SAFE_KEYS`: +Names are matched case-insensitively. A card key, or a name ending in a CVV or +credential word (`card_number`, `pan_no`, `card_cvv`, `db_password`, +`oauth_token`, `api_key`, …), names the value itself and cannot be listed: +`configure_masking_safe_keys` raises, and from the setting or env var it is +dropped with a warning, stays masked, and fails the Django boot check. A flag +or status about one (`cvv_required`, `tokenization_status`) can be listed, and +so can an expiry key, though it is safe already: nothing masks it. Ottu +services use `ecsctx.contrib.ottu.masking.SAFE_KEYS`, and list its +`WALLET_RULES` as value rules beside them (below): ```python -from ecsctx.contrib.ottu.masking import SAFE_KEYS as OTTU_SAFE_KEYS +from ecsctx.contrib.ottu.masking import SAFE_KEYS as OTTU_SAFE_KEYS, WALLET_RULES ECSCTX_MASK_SAFE_KEYS = [*OTTU_SAFE_KEYS] +ECSCTX_MASK_VALUE_RULES = [*WALLET_RULES] ``` +### Value rules (a service's own shapes) + +Some values are known by their shape rather than by the key they sit under — +a payment method's encrypted token, say — and which shapes those are is a +service's to say: ecsctx names none. A service lists its value rules, and the +engine asks them about every mapping the key walk meets (under any key), every +string that is JSON text, and every JSON object written in free text or in a +body `redact_body` masks, JSON in an XML element's text included. A matching +value becomes the rule's label, in every pack: never hashed, and left as it is +by a later pass. With none configured, none of this runs. + +A rule is any object with a `field_type` (the label it becomes: `"sad"` is +`[SAD-MASKED]`), `matches(value)` — called with a mapping, JSON text parsed +once — and, optionally, `hints`: literal strings a matching value's text holds +at least one of, so text holding none is never parsed to ask. `ValueRule` is +one: + +```python +from ecsctx.masking import ValueRule + +def is_vault_blob(value): + return value.get("kind") == "vault-blob" and "payload" in value + +VAULT_BLOB = ValueRule("sad", is_vault_blob, hints=("vault-blob",)) +``` + +```python +# 1. Django settings.py: rule objects, or dotted paths to a rule or to a collection of them +ECSCTX_MASK_VALUE_RULES = ["myservice.masking.VAULT_BLOB"] + +# 2. Env var, comma-separated dotted paths +# ECSCTX_MASK_VALUE_RULES="myservice.masking.VAULT_BLOB" + +# 3. Programmatic, at startup (wins over both) +from ecsctx.masking import configure_masking_value_rules +configure_masking_value_rules([VAULT_BLOB]) +``` + +An item that does not import or is not a rule makes +`configure_masking_value_rules` raise; from the setting or env var it is +dropped with a warning and fails the Django boot check, and the others still +apply. A rule that raises leaves the record as `[MASKING-FAILED: …]`, never +an exception out of the log call. + +Ottu's are `ecsctx.contrib.ottu.masking.WALLET_RULES`: an Apple Pay token's +`paymentData` (`version` `EC_v1` or `RSA_v1`, with the encrypted `data`) and +a Google Pay payment method token (`protocolVersion` `ECv1`, `ECv2` or +`ECv2SigningOnly`, with a `signedMessage`) are `[SAD-MASKED]` whole — under +`paymentData`, TAP's `token_data`, MPGS's `paymentToken` (a JSON string), +Google Pay's `tokenizationData.token`, KPay's ``, or in a message. An +Ottu service lists them in `ECSCTX_MASK_VALUE_RULES` next to its +`ECSCTX_MASK_SAFE_KEYS` (above), or sets +`ECSCTX_MASK_VALUE_RULES=ecsctx.contrib.ottu.masking.WALLET_RULES`. Core +masks no wallet token of its own accord: without them, one is read as any +other value is. + ### Path exemptions Some non-PII fields share a name with a sensitive key — e.g. a payment catalog's `payment_methods[*].name` ("KNET") would otherwise be tokenized. The whitelist above is key-name based and global; for finer control, exempt specific **JSON paths** from key-based tokenization. (Email/phone scrubbing still runs on exempted paths, so a real email never slips through.) @@ -1347,7 +1487,7 @@ PII_VAULT_TIMEOUT=10 # HTTP timeout for Vault ca 2. The structure is walked recursively, tracking each value's JSON path. A string that is a JSON object or list (up to 64 KiB, e.g. a callback's raw body) is parsed and walked the same way, then written back 3. A sensitive-key string value is tokenized (HMAC-SHA-256) — unless its key is whitelisted, listed in the service's [safe keys](#safe-keys-a-services-own-names), or its path is exempted (see [Path exemptions](#path-exemptions)) 4. Every string value is also scanned for email/phone patterns and tokenized (defense in depth, even on exempted paths) -5. Auth header values are masked (truncated, not encrypted) +5. Auth header values (`Authorization`, `Proxy-Authorization`, cookies) are masked as credentials (`mask_secret`): the scheme and the credential as one value, its token, or `[SECRET-MASKED]` without a keyset — never truncated, never encrypted 6. Values are normalized before tokenization (emails lowercased, phones to E.164) ### Example Output @@ -1401,15 +1541,15 @@ This is the most frequently broken field. Every `except` block tempts you: ```python # WRONG — will cause ES mapping conflict except Exception as e: - log.error("something_failed", error=str(e)) + log.error("something failed", error=str(e)) # CORRECT — ECS-compliant dict except Exception as e: - log.error("something_failed", error={"message": str(e)}) + log.error("something failed", error={"message": str(e)}) # EVEN BETTER — include exception type except requests.HTTPError as e: - log.error("api_call_failed", error={ + log.error("api call failed", error={ "message": str(e), "type": type(e).__name__, }) @@ -1421,10 +1561,10 @@ Only ECS reserved names need the dict treatment. The `namespace_ecs_fields` proc ```python # "merchant_id" stays at root (allowlisted) -log.info("payment_started", merchant_id="acme") +log.info("payment started", merchant_id="acme") # "disclosure_pk" is not allowlisted — goes into extra.disclosure_pk -log.info("disclosure_created", disclosure_pk=42) +log.info("disclosure created", disclosure_pk=42) ``` ### Elasticsearch Indexing: `labels` vs `extra` @@ -1437,7 +1577,7 @@ log.info("disclosure_created", disclosure_pk=42) bind_logging_context(labels={"env": "prod", "region": "us-east-1"}) # Good: non-filterable details as bare kwargs (auto-wrapped into extra) -log.info("payment_processed", amount=100, currency="KWD") +log.info("payment processed", amount=100, currency="KWD") # Output: {..., "extra": {"amount": 100, "currency": "KWD"}} ``` @@ -1468,17 +1608,24 @@ stdlib logs bypass the entire structlog processor chain (context injection, ECS ### Mistake #2: f-string Log Messages ```python -# ❌ WRONG — dynamic data in message, unsearchable, unaggregatable +# ❌ WRONG — the values rendered into the message: every line is different log.info(f"Payment processed for merchant {merchant} amount {amount}") -# ❌ ALSO WRONG — printf-style formatting -log.error("OAuth token exchange failed: shop=%s response=%r", shop, response) +# ✅ CORRECT — lowercase prose, a constant template, lazy %s arguments +log.error("oauth token exchange failed for %s: %s", shop, response.status_code) -# ✅ CORRECT — static event name + structured kwargs -log.info("payment_processed", merchant=merchant, amount=amount) +# ✅ ALSO CORRECT — a constant message, the values as structured fields +log.info("payment processed", merchant=merchant, amount=amount) ``` -**Why it matters:** In Kibana, you search by `message: "payment_processed"`. With f-strings, every log line has a different message — you can't aggregate, alert, or build dashboards. +**Why it matters:** The message is what Kibana aggregates and alerts on, and +what Sentry groups by. A `%s` template stays constant however its arguments +vary — logging fills them in only when the line is emitted, and masking reads +them as values — while an f-string makes every line a message of its own. The +message is lowercase prose a person reads, not an identifier: the event +itself is named by `ecs_event=` ([section 23](#23-declared-events-ecsctxevents)), +so a snake_case "event name" in the message is neither. Ottu's services +(Connect, Ottu PG) gate this style. --- @@ -1486,10 +1633,10 @@ log.info("payment_processed", merchant=merchant, amount=amount) ```python # ❌ WRONG — flat string breaks ECS error field mapping -log.exception("invalid_data", error=str(error)) +log.exception("invalid data", error=str(error)) # ✅ CORRECT — ECS-compliant dict -log.exception("invalid_data", error={"message": str(error)}) +log.exception("invalid data", error={"message": str(error)}) ``` --- @@ -1497,13 +1644,13 @@ log.exception("invalid_data", error={"message": str(error)}) ### Mistake #4: `log.exception(e)` — Exception as Message ```python -# ❌ WRONG — exception object as first arg, not a structured event name +# ❌ WRONG — exception object as first arg, not a constant message except Exception as e: log.exception(e) -# ✅ CORRECT — static event name, structlog auto-captures exception info +# ✅ CORRECT — a constant message, structlog auto-captures exception info except Exception as e: - log.exception("payment_processing_failed") + log.exception("payment processing failed") ``` --- @@ -1513,7 +1660,7 @@ except Exception as e: ```python # ❌ WRONG — first log has no merchant_id or payment context def post(self, request, merchant_id, client_payment_id): - log.info("acknowledgement_received", + log.info("acknowledgement received", merchant_id=merchant_id, client_payment_id=client_payment_id, ) @@ -1525,7 +1672,7 @@ def post(self, request, merchant_id, client_payment_id): "merchant_id": merchant_id, settings.APP_NAME: {"client_payment_id": client_payment_id}, }) - log.info("acknowledgement_received") + log.info("acknowledgement received") ``` --- @@ -1535,11 +1682,11 @@ def post(self, request, merchant_id, client_payment_id): ```python # ❌ WRONG — session_id already in context, passed again as kwarg bind_logging_context(session_id=session_id) -log.info("notification_received", session_id=session_id) # redundant! +log.info("notification received", session_id=session_id) # redundant! # ✅ CORRECT — it's already in context bind_logging_context(session_id=session_id) -log.info("notification_received") +log.info("notification received") ``` --- @@ -1577,10 +1724,10 @@ See the [Core Rules](#4-core-rules-field-placement-reference) table for the comp ```python # ❌ WRONG — Sentry alert for missing pg_codes (customer config problem) -log.error("pg_codes_not_found") +log.error("pg codes not found for %s", merchant_id) -# ✅ CORRECT — not our fault, not worth waking someone up -log.info("pg_codes_not_found") +# ✅ CORRECT — not our fault, not worth waking someone up; still a failure +log.warning("pg codes not found for %s", merchant_id) ``` --- @@ -1615,21 +1762,26 @@ This isn't just style — it directly affects Sentry alert volume and on-call fa Is this a system failure that needs human attention? ├── YES → log.error (triggers Sentry alert) └── NO - ├── Is this a customer config problem? → log.info - ├── Will the task retry? → log.info (alert after retries exhausted) - ├── Is this expected? (auth fail, 404) → log.info + ├── Did something fail, expected or not? (customer config, a retry, + │ auth fail, a rejected callback) → log.warning + ├── Normal operations? → log.info └── Debug/development info? → log.debug ``` > **The golden rule: `log.error` = "Wake someone up."** If it's not worth waking someone up, it's not `log.error`. +A failure is never below `warning`: the contract validator (`event_contract`, +[section 23](#the-log-contract-event_contract-processor)) reports any event +logged with `outcome=failure` at `debug` or `info` (`failure_below_warning`), +and the catalogue's expected failures declare `failure_level="warning"`. + | Situation | Level | Reasoning | |-----------|-------|-----------| | System/infra failure (DB down, API 500) | `log.error` | Needs Sentry alert + on-call | | Business logic failure (max retries exceeded) | `log.error` | System failed its job | -| Customer config error (merchant not found) | `log.info` | Not our fault | -| Retry-able failure (temporary network blip) | `log.info` | Task will retry | -| Auth failure (invalid token, bad HMAC) | `log.info` | Expected, handled | +| Customer config error (merchant not found) | `log.warning` | Not our fault, but a failure | +| Retry-able failure (temporary network blip) | `log.warning` | Task will retry; `error` once retries are exhausted | +| Auth failure (invalid token, bad HMAC) | `log.warning` | Expected and handled, but a failure | | Normal operations (webhook received) | `log.info` | Operational visibility | | Verbose debugging (raw payloads) | `log.debug` | Filtered in production | @@ -1689,14 +1841,14 @@ Check these fields in your JSON output: import structlog log = structlog.get_logger(__name__) -log.info("test_pii", customer_name="John Doe", email="john@example.com", amount=100) +log.info("pii masking check", customer_name="John Doe", email="john@example.com", amount=100) ``` Expected stdout: ```json { - "message": "test_pii", + "message": "pii masking check", "customer_name": "ptok:v1:...", "email": "ptok:v1:...", "amount": 100 @@ -1709,7 +1861,7 @@ If `customer_name` shows `"John Doe"` in plain text, check that `mask_sensitive_ ```python # In a view, dispatch a task and check worker stdout -log.info("dispatching_task") +log.info("dispatching task") my_task.apply_async(args=[...]) # In the Celery worker output, the task log should have: @@ -1737,8 +1889,8 @@ docker compose logs vector ### Step 6: Verify in Kibana 1. Go to Kibana → Discover -2. Select the data stream: `logs-{PROJECT_NAME}-{ENVIRONMENT}` -3. Search: `message: "test_pii"` +2. Select the data stream: `logs-{PROJECT_NAME}-{ENVIRONMENT}` (at Ottu, `logs-*`, or `logs-pci-` for a PCI service) +3. Search: `message: "pii masking check"` 4. Verify fields are nested correctly (`trace.id`, not flat `trace_id`) 5. Verify PII is tokenized (`ptok:v1:...`, not plain text) @@ -1858,7 +2010,7 @@ services: ### Data Stream Naming -Your logs land in Elasticsearch under: +With the template above, your logs land in Elasticsearch under: ``` logs-{PROJECT_NAME}-{ENVIRONMENT} @@ -1869,6 +2021,10 @@ Examples: - `logs-event-backend-staging` - `logs-checkout-dev` +Ottu's services do not use this layout: they ship to daily `logs-YYYY.MM.DD` +indices, and a PCI service (Ottu PG) to `logs-pci-`, from log files Vector +tails. + If you use a `common-logs` ingest pipeline, it can enforce ECS field types so malformed fields (e.g., flat `error` string) get flagged at ingest time. --- @@ -2042,7 +2198,7 @@ class LoggingContext: { "@timestamp": "2025-01-13T10:30:00.000Z", "ecs.version": "1.12.0", - "message": "payment_processed", + "message": "payment processed", "log.level": "info", "log.logger": "core.payment.views", "trace": { @@ -2262,7 +2418,7 @@ There is one way to log an event: your own logger, with the event's payload. ```python logger.info( - "Gateway replied in %s ms", elapsed_ms, + "gateway replied in %s ms", elapsed_ms, ecs_event=PG_RESPONSE_RECEIVED.ecs(outcome=Outcome.SUCCESS, duration_ns=elapsed_ns), session_id=sid, payment={"pg_code": "mpgs"}, diff --git a/ecsctx/CLAUDE.md b/ecsctx/CLAUDE.md index dc23a94..2dc9e68 100644 --- a/ecsctx/CLAUDE.md +++ b/ecsctx/CLAUDE.md @@ -12,15 +12,15 @@ Core structured logging module with ECS field mapping and PII/PCI masking. - `logging_context` supports nesting - inner contexts merge with outer, auto-restored on exit - `get_trace_id()` parses W3C traceparent format: extracts 32-char trace-id from `{version}-{trace-id}-{parent-id}-{flags}` - `PRIMARY_KEYS` in processors.py defines which fields stay at root vs get pushed to `extra` -- Masking has two entry points, both driving the same `MaskPIIFilter`: the filter on every handler `get_logging_config()` builds (masks the record in place — Sentry's logging integration, `handleError` and handlers that never call `format()` read the record) and `mask_sensitive_data` (structlog processor, in the formatter's own chain). The second pass is cheap: `mask_by_patterns` remembers strings already known clean, and a known-clean string is not re-parsed. A string that is a JSON object or list (≤ `JSON_PARSE_LIMIT`, 64 KiB) is parsed and walked with the key rules only, re-serialised only if a key rule changed something, then content-scanned once as text — so a JSON number holding a PAN still masks, as an unquoted marker. Never scanned (`DEFAULT_SKIP_KEYS`, `masking/filters.py`): `service`/`project`/`log` and the correlation ids `session_id`, `trace`, `span`. `user.name` is exempt from the name key rule but content-scanned. The record marker stores the packs that masked it, so a filter with more packs still masks. A record `getMessage()` could not render once masked -- a %-style template masking changes (`password=%s`: rule 3 reads the placeholder as the credential), a msg that is not a str (a lazy translation, an exception; `_template_of` is `str(msg)`, bytes decoded as masking decodes them (as `str(bytes)` it never matched the masked template, so every bytes record lost its args), None for a dict or list, a structured record, whose args, if any, stay as they are -- logging cannot format it, so as in stdlib the line is dropped and `handleError` prints the args, masked (README says so)), a number masking turned into text under `%d` (`_as_masked_text` reads that conversion as `%s`, keeping its key, `-`, width and `*`s, so the masked text renders where the number stood; rendered from the raw number, a card number glued to a word stays whole), args that never fit -- is rendered from the masked args and masked whole, its args `()` (`MaskPIIFilter._mask_rendered`), or is the `[MASKING-FAILED: …]` marker: before, `getMessage()` raised, the handler dropped the line and `handleError` printed the args to stderr. It renders from the masked args, not the raw ones, so a dict arg is still masked by its keys (from the raw ones a card number under `card_number` reached only the text rules, and the default pack has no card rule). A template masking leaves as it is, and that renders, keeps its args, and Sentry's grouping by it. Rendered, a record keeps no args for a later handler to mask with more packs, so the render path masks them with every pack (`ALL_PACKS`, only for a record that takes it): a default handler ahead of a pci one rendered `ref4111…` whole, glued to a word, for the pci handler to log, in a pci process and in a default one. A record that keeps its args is masked piece by piece: readers besides the formatter (Sentry's logging integration, `handleError`, a handler that never calls `format()`) render it themselves, so a value split between template and arg (`cvv=%s` with `123`) reaches them as `cvv=123`; the formatter's `mask_sensitive_data` masks the formatted line whole (`install.py`'s docstring says so). -- Two independent detection strategies inside `MaskPIIFilter`: 18 ordered content rules (`masking/patterns.py::RULES` — order is load-bearing, see the comments above `_RULE_TABLE`), grouped into packs (`default` always on, the keyed CVV rules 4, 5 and 9 among its own -- their keyword the key rule's CVV words, after `_`/`-`, `card` or a camelCase word (`vpc_CardSecurityCode`, `paymentCvv`), ending the key -- a CVV must not ship from any service, and Connect receives the saved-card CVV; `pci` -- the card rule and the bare-digit CVV rule -- and `financial_ids` opt-in via `masking/config.py`), each behind a literal pre-check; the credential rules (2, 3, 8) and the keyed CVV rules (4, 5, 9) are tried only near their words (`_near_words`: a match starts inside the `[\w-]` run holding its word, at most `_CRED_REACH`/`_CVV_REACH` before it, or on the quote before that run) -- `re.sub` tried every word boundary, and a long `a-a-a-…` cost 0.7 s per CVV rule; and key-name matching (`classify_key()`: substring on the lowercased key, fail closed, EXCEPT the three families where a substring made false positives -- a credential word must end the key (`scheme_token` yes, `schemeTokenProvisioningMode` no), `authorization` must be the whole key (`authorizationCode` is a payment verdict), and a `*name` key is a person's only if a person qualifier is in the name (`customer_name` yes, `domainName` no); a CVV word counts anywhere in the key unless its tail names something about one (`cvv_input` yes -- fail closed -- `cvv_required`/`cardSecurityCodeError` no), matched on the separator-free key; `track`/`magnetic` are SAD only when the rest of the key names the data (`raw_track` yes, `track_id` no), and a key ending in `cryptogram`/`cavv`/`ucaf` is SAD; a national id (`civil_id`, `passport`, `qid`, ...) is `ssn` in every pack; SAFE_KEYS is matched in both spellings, separators or camelCase; card keys matched precisely (`card`, a `pan` word, `cardnumber` in the key, or a key ending `cardno`/`cardnum` -- MIGS's `vpc_CardNum`; `card_id` is an id, not one); expiry is NOT classified and is listed in `SAFE_KEYS`; `sad` (track data, PIN blocks, EMV images, cryptograms) is destroyed outright like CVV and can never be listed safe; cached). A matching key masks its value. A scalar under a non-exemptable type is masked whole (a PAN-shaped credential is the label, never truncated); a CONTAINER under `card`, `cvv`, `sad` or `secret` is walked per leaf (`_WALKED_TYPES`) -- unnamed leaves take the container's type, under `cvv`/`sad` nothing is weaker than the container and only a service-listed key escapes, and a card-shaped object under a credential key is walked as `card` -- spelled out in `_mask_dict` rather than made exemptable, because `exemptable` also governs path exemptions and none of these may ever be whitelistable; a `{name, value}` pair masks `value` by the strictest type its identifiers name (`_pair`); a bare `name` under a container named by a thing (`name_context`) is not a person; booleans are never masked; a dataclass/namedtuple is masked by field then rendered back to repr text; the walk stops at `_MAX_DEPTH` and any exception replaces the message with `[MASKING-FAILED: …]` (a log call must never raise); a dict/list under a PII key (`customer`, `billing`, `contact`, `name`, …) keeps its shape and each field is masked on its own — by its own key's type, kept if a safe key (`id`), else as the container's type — so per-field tokens correlate across records. `exc_info`/`stack_info` are never masked (the rendered `error.*` is). Card keys truncate a PAN value and show what is not one (`mask_card_value`: under 12 digits reads through -- unless a group of digits and stars long enough to be a card number (12+) is in no truncation's shape, `_TRUNCATION_SHAPE`: the first six digits at most, four stars or more, the last four at most -- an upstream system's `123456****7890` and a first-four `4111********1111` included -- with a card number's groups -- four to six digits or stars, one card separator (`_CARD_SEP`) apart, as in 4-4-4-4, Amex 4-6-5 and Diners 4-6-4 -- read as one (`_CARD_GROUPS`), so another masker's `45087****001019` (the last six of 15), `4508-750*-****-1019` (read `4508750*****1019`, the first seven of 16) and `3782 822*** *0005` are refused, while a shorter group ends a run: `**** 1111 12 25` is not judged (`****1111` is eight) -- 12+ is content-scanned for a PAN and refused if none is found, if a run of card-number length is left beside the truncations -- joined by dots or slashes too, and a truncation's first six counting with the digits before it -- or if a Luhn-valid reading the rule leaves in free text shows more than its first six and last four, `_overexposes_a_card`; a value that the scan leaves as it is and that holds a truncation the card rule writes (`_WRITTEN_TRUNCATION`: the first six, stars and last four, or eight stars or more and the last four) -- its own output on a later pass -- is refused only for a card-length run, for twelve digits or more showing beside its truncations (`_digits_beside_truncations`) or around one (`_card_digits_showing`), or for digits and stars in any other shape (`_hides_as_truncations`: another masker's `4508750****001019` shows 13 of 17, `****1111 1234 5670` 12 of 16), and the first pass refuses that shape too, so every output is a fixed point (0.15.4 refused its own output there, so the formatter's second pass turned a shown truncation into `[CARD-MASKED]`). A PAN under ANY key is truncated rather than tokenized (`filters._mask_pii_leaf`, gated on `pci` among the call's packs and the process's, as is a bare int card number: in a pci process a default filter hashed one typed into the name box): a keyed hash beside a truncation of the same PAN is what FAQ 1117 forbids, and a card number typed into the name box used to produce exactly that. Under `pci`, a value a PII, secret, ssn or payment-id key would tokenize is its label, not a token, when it holds any 12+ digit run (`holds_pan_run`, fails closed -- a random hex id trips it 2-7% of the time, by length); only in a phone field is a `+`-led run of at most 15 digits (E.164) the phone number. A canonical UUID (`patterns._UUID`: 8-4-4-4-12 hex with at least one hex letter, nothing alphanumeric touching it) is the one exception to "any 12-19 digit run is a card" (the contract `tests/test_masking_false_positives.py` names): a value that is one holds no run, and the card rule leaves its digits as written in text and reads the rest of the run without them, so a card number beside it is still truncated. An all-digit string in that shape gets no exemption. -- A tokenized value is the bare token (`ptok:v1:…`); `[LABEL]` only where no token can stand (tokenization unconfigured/failing, or CVV/card, which are never tokenized; expiry is not masked at all). A null under a sensitive key stays null. `_WHOLE_TOKEN` keeps the credential text rules off a value that is already a token; like `already_tokenized` and `already_masked`, it takes only the exact shape `tokenize` emits (`ptok:v1:` + 43 base64url characters), so anything else typed after `ptok:` is masked -- and it takes none with a card-number run in its body, which `mask_by_field_type` labels. A token followed by `#` is whole: the `#` opens a URL's fragment (`redact_url`'s `?password=#access_token=…`), though `#` is otherwise part of a value. A credential value in text (rules 2, 3 and 8) runs to its delimiter, as a form value in `redact_body` does, never to the end of a fixed character class (a backslash, `@` or apostrophe left the rest in clear): unquoted (`_UNQUOTED_VALUE`), to whitespace, `&`, `;`, `,`, a closing bracket, or a closing quote -- one with nothing of the value after it, or a JSON key's `":` or an element's `/>` -- never starting with a quote or an opening bracket (a container, or a label masking wrote) and never running into a label (`_NOT_A_LABEL`: a `]` would cut it, and a label redact_body wrote after a short value made it long enough to mask again); quoted, to its unescaped closing quote, spaces and all (`_quoted_body`), a `"`-quoted one masked as the JSON string it decodes to (`_mask_quoted`), so it carries the key walk's token. Rule 3 reads a quote as opening a quoted value only when a closing one matches it and something is between them -- quotes doubled as CSV and SQL escape one (`7,"password=""s3cret"""`) are structure before an unquoted value -- and the escaped quotes around a JSON-escaped key as the key's; a value that opens with `\"` (JSON in a JSON string) runs to its matching `\"` as the inner string reads it (`_ESCAPED_QUOTED_UNIT`), spaces and all, masked as it decodes twice (`_mask_escaped_quoted`); `_unquoted_value_quote` escapes the quotes it adds around an unquoted value there. After an Authorization keyword (`_AUTH_KEYWORD`: `authorization`, `Proxy-Authorization`, `authorisation_header`; rule 3 only) the value is the scheme and the credential together (`_AUTH_SCHEME`: bearer, basic, digest, token, negotiate, api-key), so `Authorization: Bearer x` carries the token `mask_secret("Bearer x")` -- the one the header gets under its key -- instead of one token for `Bearer` shared by every record and the credential left to the bare-space rule (which needs a digit: `Basic dXNlcjpwYXNz` shipped); a scheme is never the whole value while more follows it (`Bearer [REDACTED]` stays as it is), and a token after the scheme (`Bearer ptok:v1:…`, what masking the credential alone left) is not hashed again with it. Other keywords take no scheme: `password=basic more words` is the password `basic`. A credential keyword the key rule reads as a CVV or other SAD (`cvv_token`, `pin_password`, `_credential_type`) masks its value as that type, the label, as under the key and in a URL -- never a keyed hash of a CVV. Rule 12 masks a URL's userinfo in text (`scheme://user:password@`, the password possibly empty, or a token alone -- not a user alone, `ssh://git@host`), each part through `_mask_userinfo`, which `contrib.net.redact_url` shares: as it decodes, a token at its head read as the user whole (split at its first colon, pass 2 wrote `ptok:v1:ptok:v1:…`); scheme, host and port kept. It runs before phone and email: a numeric password is no phone number, and the email rule (`_email`) never starts inside masking's own output -- a token, or a userinfo masking wrote (`_mask_userinfo(userinfo) == userinfo`, e.g. redact_url's percent-encoded `%5BSECRET-MASKED%5D`) -- so the host after it survives. A credential keyword as the user (`https://x-access-token:ghp_…@github.com`) is rule 3's first, and the value it runs to takes the host with it. Path exemptions via `configure_masking()`, Django `ECSCTX_MASK_EXEMPT_PATHS`, or env `PII_MASK_EXEMPT_PATHS` (`masking/exemptions.py`, a pattern may start at any depth of the record) apply only to the exemptable field types (email/phone/address/name/generic) and only skip the key-based match — content rules still run on the value (defense in depth). Secrets (cvv/card/secret/payment_id/pem_key/iban/jwt/ssn) are never exemptable. +- Masking has two entry points, both driving the same `MaskPIIFilter`: the filter on every handler `get_logging_config()` builds (masks the record in place — Sentry's logging integration, `handleError` and handlers that never call `format()` read the record) and `mask_sensitive_data` (structlog processor, in the formatter's own chain). The second pass is cheap: `mask_by_patterns` remembers strings already known clean, and a known-clean string is not re-parsed. What a rule set derives -- its known-clean set, `scalar_rules`, its distinct gates -- is cached on the tuple's identity (`patterns._by_identity`), never on its hash: hashing a tuple of rules hashes every compiled pattern's whole program, on every call (30 µs a string for the default pack; lru_cache paid it on every lookup, the 0.15.5 slowdown, #160054). A string that is a JSON object or list (≤ `JSON_PARSE_LIMIT`, 64 KiB) is parsed and walked with the key rules only, re-serialised only if a key rule changed something, then content-scanned once as text — so a JSON number holding a PAN still masks, as an unquoted marker. Never scanned (`DEFAULT_SKIP_KEYS`, `masking/filters.py`): `service`/`project`/`log` and the correlation ids `session_id`, `trace`, `span`. `user.name` is exempt from the name key rule but content-scanned. The record marker stores the packs that masked it, so a filter with more packs still masks. A record `getMessage()` could not render once masked -- a %-style template masking changes (`password=%s`: rule 7 reads the placeholder as the credential), a msg that is not a str (a lazy translation, an exception; `_template_of` is `str(msg)`, bytes decoded as masking decodes them (as `str(bytes)` it never matched the masked template, so every bytes record lost its args), None for a dict or list, a structured record, whose args, if any, stay as they are -- logging cannot format it, so as in stdlib the line is dropped and `handleError` prints the args, masked (README says so)), a number masking turned into text under `%d` (`_as_masked_text` reads that conversion as `%s`, keeping its key, `-`, width and `*`s, so the masked text renders where the number stood; rendered from the raw number, a card number glued to a word stays whole; it finds the conversion by counting them as CPython's `%` reads a template (`_CONVERSION`: `%%`, a key to its balancing parenthesis, flags, width, a precision whose digits may be none -- `%.f` -- one length modifier), else the count slipped and the record was the marker), args that never fit, or a line that masks differently from its masked args rendered (a value split between template and arg, `cvv=%s` with `123`) -- is rendered from the masked args and masked whole, its args `()` (`MaskPIIFilter._mask_rendered`), or is the `[MASKING-FAILED: …]` marker: before, `getMessage()` raised, the handler dropped the line and `handleError` printed the args to stderr. It renders from the masked args, not the raw ones, so a dict arg is still masked by its keys (from the raw ones a card number under `card_number` reached only the text rules, and the default pack has no card rule). A template masking leaves as it is, that renders, and whose masked args render a line with nothing more to mask, keeps its args, and Sentry's grouping by it. Rendered, a record keeps no args for a later handler to mask with more packs, so the render path masks them with every pack (`ALL_PACKS`, only for a record that takes it): a default handler ahead of a pci one rendered `ref4111…` whole, glued to a word, for the pci handler to log, in a pci process and in a default one. A record that keeps its args is masked piece by piece, and the line its masked args render holds nothing more to mask: readers besides the formatter (Sentry's logging integration, `handleError`, a handler that never calls `format()`) render it themselves, and a value split between template and arg (`cvv=%s` with `123`) no longer reaches them as `cvv=123` -- that record takes the render path (`install.py`'s docstring says so). The check renders and masks the line once per record with args; the formatter's `mask_sensitive_data` masks the same line again, a known-clean lookup when it matches. +- Two independent detection strategies inside `MaskPIIFilter`: 24 ordered content rules (`masking/patterns.py::RULES` — order is load-bearing, see the comments above `_RULE_TABLE`), grouped into packs (`default` always on, the value rule 2, the XML card, CVV and SAD element 3 and credential element 4, the `API-Key` scheme 6, the credential container 8 and the keyed CVV rules 9, 10 and 14 among its own -- their keyword the key rule's CVV words, after `_`/`-`, `card` or a camelCase word (`vpc_CardSecurityCode`, `paymentCvv`), ending the key -- a CVV must not ship from any service, and Connect receives the saved-card CVV; `pci` -- the card rule and the bare-digit CVV rules, 23 beside a card number or its truncation and 24 after a CVV word -- and `financial_ids` opt-in via `masking/config.py`), each behind a literal pre-check; the credential rules (5, 6, 7, 8, 13) and the CVV word rules (9, 10, 14, 24) are tried only near their words (`_near_words`: a match starts inside the `[\w-]` run holding its word, at most `_CRED_REACH`/`_CVV_REACH` before it, or on the quote before that run) -- `re.sub` tried every word boundary, and a long `a-a-a-…` cost 0.7 s per CVV rule; and key-name matching (`classify_key()`: substring on the lowercased key, fail closed, EXCEPT the three families where a substring made false positives -- a credential word must end the key (`scheme_token` yes, `schemeTokenProvisioningMode` no), `authorization` must be the whole key (`authorizationCode` is a payment verdict), and a `*name` key is a person's only if a person qualifier is in the name (`customer_name` yes, `domainName` no); a CVV word counts anywhere in the key unless its tail names something about one (`cvv_input` yes -- fail closed -- `cvv_required`/`cardSecurityCodeError` no), matched on the separator-free key; `track`/`magnetic` are SAD only when the rest of the key names the data (`raw_track` yes, `track_id` no), and a key ending in `cryptogram`/`cavv`/`ucaf` is SAD; a national id (`civil_id`, `passport`, `qid`, ...) is `ssn` in every pack; SAFE_KEYS is matched in both spellings, separators or camelCase; card keys matched precisely (`card`, a `pan` word, `cardnumber` in the key, or a key ending `cardno`/`cardnum` -- MIGS's `vpc_CardNum`; `card_id` is an id, not one); expiry is NOT classified and is listed in `SAFE_KEYS`; `sad` (track data, PIN blocks, EMV images, cryptograms) is destroyed outright like CVV and can never be listed safe; cached). A matching key masks its value. A scalar under a non-exemptable type is masked whole (a PAN-shaped credential is the label, never truncated); a CONTAINER under `card`, `cvv`, `sad` or `secret` is walked per leaf (`_WALKED_TYPES`) -- unnamed leaves take the container's type, under `cvv`/`sad` nothing is weaker than the container and only a service-listed key escapes, and a card-shaped object under a credential key is walked as `card` -- spelled out in `_mask_dict` rather than made exemptable, because `exemptable` also governs path exemptions and none of these may ever be whitelistable; a `{name, value}` pair masks `value` by the strictest type its identifiers name (`_pair`); a bare `name` under a container named by a thing (`name_context`) is not a person; booleans are never masked; a dataclass/namedtuple is masked by field then rendered back to repr text; the walk stops at `_MAX_DEPTH` and any exception replaces the message with `[MASKING-FAILED: …]` (a log call must never raise); a dict/list under a PII key (`customer`, `billing`, `contact`, `name`, …) keeps its shape and each field is masked on its own — by its own key's type, kept if a safe key (`id`), else as the container's type — so per-field tokens correlate across records. `exc_info`/`stack_info` are never masked (the rendered `error.*` is). Card keys truncate a PAN value and show what is not one (`mask_card_value`: under 12 digits reads through -- unless a group of digits and stars long enough to be a card number (12+) is in no truncation's shape, `_TRUNCATION_SHAPE`: the first six digits at most, four stars or more, the last four at most -- an upstream system's `123456****7890` and a first-four `4111********1111` included -- with a card number's groups -- four to six digits or stars, one card separator (`_CARD_SEP`) apart, as in 4-4-4-4, Amex 4-6-5 and Diners 4-6-4 -- read as one (`_CARD_GROUPS`), so another masker's `45087****001019` (the last six of 15), `4508-750*-****-1019` (read `4508750*****1019`, the first seven of 16) and `3782 822*** *0005` are refused, while a shorter group ends a run: `**** 1111 12 25` is not judged (`****1111` is eight) -- 12+ is content-scanned for a PAN and refused if none is found, if a run of card-number length is left beside the truncations -- joined by dots or slashes too, and a truncation's first six counting with the digits before it -- or if a Luhn-valid reading the rule leaves in free text shows more than its first six and last four, `_overexposes_a_card`; a value that the scan leaves as it is and that holds a truncation the card rule writes (`_WRITTEN_TRUNCATION`: the first six, stars and last four, or eight stars or more and the last four) -- its own output on a later pass -- is refused only for a card-length run, for twelve digits or more showing beside its truncations (`_digits_beside_truncations`) or around one (`_card_digits_showing`), or for digits and stars in any other shape (`_hides_as_truncations`: another masker's `4508750****001019` shows 13 of 17, `****1111 1234 5670` 12 of 16), and the first pass refuses that shape too, so every output is a fixed point (0.15.4 refused its own output there, so the formatter's second pass turned a shown truncation into `[CARD-MASKED]`). A PAN under ANY key is truncated rather than tokenized (`filters._mask_pii_leaf`, gated on `pci` among the call's packs and the process's, as is a bare int card number: in a pci process a default filter hashed one typed into the name box): a keyed hash beside a truncation of the same PAN is what FAQ 1117 forbids, and a card number typed into the name box used to produce exactly that. Under `pci`, a value a PII, secret, ssn or payment-id key would tokenize is its label, not a token, when it holds any 12+ digit run (`holds_pan_run`, fails closed -- a random hex id trips it 2-7% of the time, by length); only in a phone field is a `+`-led run of at most 15 digits (E.164) the phone number. An email or phone number the text rules find is judged the same way (`tokens._PAN_CHECKED_TYPES`: secret, payment_id, email, phone; a phone as written, `phone=True`), so with `pci` `4111111111111111@example.com` and `+4111111111111111` are their labels, not keyed hashes. A canonical UUID (`patterns._UUID`: 8-4-4-4-12 hex with at least one hex letter, nothing alphanumeric touching it) is the one exception to "any 12-19 digit run is a card" (the contract `tests/test_masking_false_positives.py` names): a value that is one holds no run, and the card rule leaves its digits as written in text and reads the rest of the run without them, so a card number beside it is still truncated. An all-digit string in that shape gets no exemption. +- A tokenized value is the bare token (`ptok:v1:…`); `[LABEL]` only where no token can stand (tokenization unconfigured/failing, or CVV/card, which are never tokenized; expiry is not masked at all). A null under a sensitive key stays null. `_WHOLE_TOKEN` keeps the credential text rules off a value that is already a token; like `already_tokenized` and `already_masked`, it takes only the exact shape `tokenize` emits (`ptok:v1:` + 43 base64url characters), so anything else typed after `ptok:` is masked -- and it takes none with a card-number run in its body, which `mask_by_field_type` labels. A token followed by `#` is whole: the `#` opens a URL's fragment (`redact_url`'s `?password=#access_token=…`), though `#` is otherwise part of a value. A credential value in text (rules 5, 7 and 13) runs to its delimiter, as a form value in `redact_body` does, never to the end of a fixed character class (a backslash, `@` or apostrophe left the rest in clear), and never into an end tag (`_NOT_AN_END_TAG`; a quoted value neither, `_quoted_value_body`: in `password=abc` it took `` -- never starting with a quote or an opening bracket (a container, or a label masking wrote) and never running into a label (`_NOT_A_LABEL`: a `]` would cut it, and a label redact_body wrote after a short value made it long enough to mask again); quoted, to its unescaped closing quote, spaces and all (`_quoted_body`), a `"`-quoted one masked as the JSON string it decodes to (`_mask_quoted`), so it carries the key walk's token. Rule 7 reads a quote as opening a quoted value only when a closing one matches it and something is between them -- quotes doubled as CSV and SQL escape one (`7,"password=""s3cret"""`) are structure before an unquoted value -- and the escaped quotes around a JSON-escaped key as the key's; a value that opens with `\"` (JSON in a JSON string) runs to its matching `\"` as the inner string reads it (`_ESCAPED_QUOTED_UNIT`), spaces and all, masked as it decodes twice (`_mask_escaped_quoted`); `_unquoted_value_quote` escapes the quotes it adds around an unquoted value there. After an Authorization keyword (`_AUTH_KEYWORD`: `authorization`, `Proxy-Authorization`, `authorisation_header`; rule 7 only) the value is the scheme and the credential together (`_AUTH_SCHEME`: bearer, basic, digest, token, negotiate, api-key), so `Authorization: Bearer x` carries the token `mask_secret("Bearer x")` -- the one the header gets under its key -- instead of one token for `Bearer` shared by every record and the credential left to the bare-space rule (which needs a digit: `Basic dXNlcjpwYXNz` shipped); a scheme is never the whole value while more follows it (`Bearer [REDACTED]` stays as it is), and a token after the scheme (`Bearer ptok:v1:…`, what masking the credential alone left) is not hashed again with it. Rule 6 does the same for Ottu's `API-Key` scheme word standing alone, without its `Authorization:` (not ending a header name, `X-API-Key`): the scheme and any value to its delimiter -- no digit or length guard, the word is the evidence (1 in 1,550 of Ottu PG's Fernet keys has no digit) -- are one value, `mask_secret("API-Key ")`, the header's token; it runs before rule 7, which would otherwise mask a `password=` inside the value first, and leaves a field masking wrote there (`API-Key password=`). Rule 13 leaves a value whose digits are all in masking's own output (a whole token, a label, a truncation): `Bearer password=`, which redact_body wrote, was hashed again, a token of a token; a credential glued to a key name keeps its own digits (`Token abc123abc123api_key=…`) and is still hashed. Rule 8 masks a container after a credential keyword whole (`password=['a', 'b']`, a rendered `%(pw)s`; a repr's quoted key keeps it a string), bracket-aware and nested three deep, or to the end of the line when never closed -- not after a JSON key (the key walk masked that container by its keys: the saved card under `token`), nor one holding only masking's own output (`_masked_already`, a record's field). Rule 3 masks an XML element named as the key walk names a card, CVV or SAD (`mask_card_elements`: `classify_key` on its local name, every pack on), in every pack: a CVV's or SAD's text, children and all, its label; a card's text a card value (`mask_card_value`: a test card that fails Luhn truncated without `pci`), and a card object (`` holding elements, CyberSource's) field by field -- a CVV or SAD element its label, every other element's text a card value, so `` truncates and an expiry reads through. It runs before rule 4, so a `` inside `` is its label, not part of the text the credential's token hashes. Rule 4 masks an XML element named by a credential keyword (`…`), to its end tag, children and all, as it decodes (`_element_text`: CDATA, entities); a start tag no end tag closes is no element (a route, `/v1/cards//`). Rules 3 and 4 are walks, not regexes (`_mask_elements`; the pattern hands the walk the text from its first `<`): a text's end tags are indexed once (`_end_tags`) and a start tag's found by name -- a lazy regex looked for one from every start tag, and 160 KB of `` took 12 s. Rule 2 gives a JSON object a service's value rule matches (value rules, below), written as JSON or a repr in text or as JSON in a JSON string, the rule's label, quoted where it was a JSON value; the key walk does to one as a mapping or JSON text under any key (`filters._value_label`), and to JSON text holding one deeper under a key that would hash or show it (`label_within`). Other keywords take no scheme: `password=basic more words` is the password `basic`. A credential keyword the key rule reads as a CVV or other SAD (`cvv_token`, `pin_password`, `_credential_type`) masks its value as that type, the label, as under the key and in a URL -- never a keyed hash of a CVV. Rule 17 masks a URL's userinfo in text (`scheme://user:password@`, the password possibly empty, or a token alone -- not a user alone, `ssh://git@host`, unless it is shaped like a card number or holds a card-number run: then `[SECRET-MASKED]`, as redact_url gives it), each part through `_mask_userinfo`, which `contrib.net.redact_url` shares: as it decodes, a token at its head read as the user whole (split at its first colon, pass 2 wrote `ptok:v1:ptok:v1:…`); scheme, host and port kept. It runs before phone and email: a numeric password is no phone number, and the email rule (`_email`) never starts inside masking's own output -- a token, or a userinfo masking wrote (`_mask_userinfo(userinfo) == userinfo`, e.g. redact_url's percent-encoded `%5BSECRET-MASKED%5D`) -- so the host after it survives. A credential keyword as the user (`https://x-access-token:ghp_…@github.com`) is rule 7's first, and the value it runs to takes the host with it. Path exemptions via `configure_masking()`, Django `ECSCTX_MASK_EXEMPT_PATHS`, or env `PII_MASK_EXEMPT_PATHS` (`masking/exemptions.py`, a pattern may start at any depth of the record) apply only to the exemptable field types (email/phone/address/name/generic) and only skip the key-based match — content rules still run on the value (defense in depth). Secrets (cvv/card/secret/payment_id/pem_key/iban/jwt/ssn) are never exemptable. - PII tokenization supports two providers (`PII_PROVIDER=file|vault`). File provider reads mounted keysets; Vault provider authenticates via AppRole and fetches from KV v2. Access mode (`PII_ACCESS=tokenize|full`) enforces least privilege. Auto-configures lazily from env vars. - When PII is not configured, `safe_tokenize()` (`masking/tokens.py`, re-exported from `processors`/`ecsctx`; the log-safe wrapper over `pii.tokenize`) returns `[PII_REDACTED]` — `mask_by_field_type()` catches that and falls back to `[LABEL]` rather than embedding it. That fallback is `safe_tokenize`'s documented contract; nothing else emits a fixed marker where a token can stand. -- Every path a credential takes ends in `mask_by_field_type(v, "secret")` — the key walk, the credential text rules, `contrib/django/routes.py`, `contrib/net` — and it returns `[SECRET-MASKED]`, never a token, for a PAN-shaped value (`pan_shaped`, imported inside the function: `patterns` imports `tokens` as it loads) and for a placeholder another masker left (`tokens._PLACEHOLDER`: `[REDACTED]`, `[PII_REDACTED]`, stars, `Bearer ****`), and for a truncation the card rule writes (`tokens._SHOWN_TRUNCATION`, `411111******1111` as written or decoded: ten digits of a saved card's gateway token, which never passes through as masking's own output -- rendered from its argument under pci, `token=%s` showed them where the key walk gave the label), which would otherwise hash into one token shared by every record. With `pci` among the call's packs, and the process's (`config.packs_in_force()`, imported inside the function as well, since `config` imports `patterns`: the packs of the masking call in progress -- a `MaskPIIFilter`'s own, which every call that builds its context sets in a ContextVar with `config.call_packs` -- joined with the process's `get_masking_packs()`, so in a pci-configured process every filter labels such a value, a filter built with pci labels it in any process, and a default handler ahead of a pci one no longer hashes what the pci handler then logs), a credential or payment id that holds a card-number run anywhere (`holds_pan_run`) is its label too, on every path -- the text rules, `mask_secret`, `contrib/net`, a route parameter -- as the key walk's own `pci` branch (`filters._mask_pii_leaf`, on the call's packs and the process's, `filters._pci_in_force`) makes any PII leaf; without `pci` among the call's packs and the process's it is hashed, an accepted residual: a default-pack service receives no card numbers, and the label would stand where a token can for ~7% of Connect's 64-hex HMAC signatures. Masking's own output passes through (`already_masked`) unless a card-number run is in it: `ptok:v1:` typed before a card number is in a token's exact shape, and is the label, for any type and in every pack (`tokens._CARDLESS_TOKEN` is that shape without one; `_WHOLE_TOKEN` takes only it). Both are judged on the value as `tokenize` hashes it (`normalize_value`: surrounding whitespace and one layer of matching quotes dropped), so `'"4111…"'` is not a keyed hash of the bare card number, and on it as a URL or form encoding decodes it (`unquote_plus`, as `redact_url` and `redact_body` read it), so `%224111…%22` or `4111+1111+…` in text is the label in every pack, as a `%5BSECRET-MASKED%5D` redact_url wrote is masked already; and a value that is masking's own output once normalized (a label, token or truncation in quotes, as the credential text rule leaves `password="[SECRET-MASKED]"`) is returned as it is, never hashed into one shared token. `mask_secret(value)` (`masking/patterns.py`, exported from `ecsctx.masking` and `ecsctx` with `mask_card_value`) is that for a value outside a mapping: `None`/bools pass, anything else is masked as `str(value)`. `_mask_pii_leaf` still asks `pan_shaped` for a secret before the `pci` branch, which would truncate it. `contrib/net` masks with `mask_secret` (no `[REDACTED]` since 0.15.4): `redact_url` masks literal `secrets` in one pass, longest first (a replacement is never scanned again, so a short secret can't mangle the token a longer one became), then masks the userinfo part by part (`_mask_userinfo`, a label's brackets percent-encoded: `urlsplit` refuses brackets in a netloc whose host is not IPv6, so the result would not parse again, nor in `ecs_url`) and rewrites a credential query or fragment param in place, unencoded, leaving every other param byte for byte, and rebuilding the URL only when something changed; a query, fragment or form key the key rules call a card, CVV or SAD (`classify_key(unquote_plus(key), ALL_PACKS)`, `_card_field_type`) has its value masked by that type -- `mask_card_value`, or the label, never a token -- in `redact_url` and in `redact_body`'s form pass (`_FORM_FIELD`, run on any body with an `=`, before the credential-key pre-check); `redact_body` reads a JSON value escape-aware, past any escaped character and to the end of the text when no quote closes it (a body `json.loads` rejects is the one that reaches this rule as text), and masks what it decodes to, or the raw text when it does not decode; its closing quote is looked for, not consumed, since in malformed `""password": "…"` it also opens the next key; and a form value runs to the next `&` or whitespace, as in 0.15.3 (every reading that ended it at a quote left the rest of some secret in clear). `_split_form_value` keeps only the structure at its two ends as written -- a quote or an escaped one (at its head, a run of them: `password=""x`, quotes doubled as CSV escapes one, and a run of two or more of either kind, `password=''x` -- a lone `'` is the value's own, a quoted value when another closes it), and `}`, `]`, `,`, a JSON key's `":`, `>`, and `/>` after a quote (after an unquoted value a `/` right after a token reads to the credential text rule as more credential, so only `>` stays) -- and takes a label, token, truncation or placeholder followed only by structure whole (`[SECRET-MASKED],` is not `[SECRET-MASKED` and `],`). The value between them is JSON-unescaped only when it sits between escaped quotes (`\"…\"`, in a JSON string; never in a raw form body), is `[SECRET-MASKED]` if it `holds_pan_run` (asked before any pass-through: a token-shaped value can carry a card number), and otherwise goes through `mask_secret`, being written back exactly as captured when that leaves it as it is (empty, or masked already). It is decoded as form encoding reads it (`unquote_plus`: `+` is a space), so outside a form body a secret holding `+` or `%xx` gets a different token than under a key; nothing leaks. The JSON rule gives every value that `holds_pan_run` the same label, closed or run on to the end of the text, before `mask_secret` (which, without `pci`, hashes one that is not in a token's shape). When its credential passes change the text, `redact_body` runs its card/CVV/SAD field pass once more: masking a credential can uncover a field the first pass read as part of the one holding it (`"password": "password="paymentCvv=…`), so one call leaves nothing a second would mask. An accepted residual, the same at 354f70f: with a keyset, the text rules then `redact_body` is not a fixed point where `redact_body` hashes a form value right after a scheme word that the text rules left (`Bearer password=[SECRET-MASKED]"x`, `Bearer password='`) -- rule 8 reads `password=` as the scheme's credential and hashes it again, a token of a token, correlation only (pinned as a strict xfail, `test_a_scheme_words_rule_reads_what_redact_body_wrote_again`); skipping `key=` in rule 8 would let a credential glued to a key name through (`Token abc123abc123api_key=…`). +- Every path a credential takes ends in `mask_by_field_type(v, "secret")` — the key walk, the credential text rules, `contrib/django/routes.py`, `contrib/net` — and it returns `[SECRET-MASKED]`, never a token, for a PAN-shaped value (`pan_shaped`, imported inside the function: `patterns` imports `tokens` as it loads) and for a placeholder another masker left (`tokens._PLACEHOLDER`: `[REDACTED]`, `[PII_REDACTED]`, stars, `Bearer ****`), and for a truncation the card rule writes (`tokens._SHOWN_TRUNCATION`, `411111******1111` as written or decoded, or any truncation after an auth scheme, `tokens._TRUNCATION_AFTER_A_SCHEME`, `Bearer 411111******1111`, which was a token of the two: ten digits of a saved card's gateway token, which never passes through as masking's own output -- rendered from its argument under pci, `token=%s` showed them where the key walk gave the label), which would otherwise hash into one token shared by every record. With `pci` among the call's packs, and the process's (`config.packs_in_force()`, imported inside the function as well, since `config` imports `patterns`: the packs of the masking call in progress -- a `MaskPIIFilter`'s own, which every call that builds its context sets in a ContextVar with `config.call_packs` -- joined with the process's `get_masking_packs()`, so in a pci-configured process every filter labels such a value, a filter built with pci labels it in any process, and a default handler ahead of a pci one no longer hashes what the pci handler then logs), a credential, payment id, email or phone number that holds a card-number run anywhere (`holds_pan_run`; a phone's `+`-led run up to 15 digits is the number) is its label too, on every path -- the text rules, `mask_secret`, `contrib/net`, a route parameter -- as the key walk's own `pci` branch (`filters._mask_pii_leaf`, on the call's packs and the process's, `filters._pci_in_force`) makes any PII leaf; without `pci` among the call's packs and the process's it is hashed, an accepted residual: a default-pack service receives no card numbers, and the label would stand where a token can for ~7% of Connect's 64-hex HMAC signatures. Masking's own output passes through (`already_masked`) unless a card-number run is in it: `ptok:v1:` typed before a card number is in a token's exact shape, and is the label, for any type and in every pack (`tokens._CARDLESS_TOKEN` is that shape without one; `_WHOLE_TOKEN` takes only it). Both are judged on the value as `tokenize` hashes it (`normalize_value`: surrounding whitespace and one layer of matching quotes dropped), so `'"4111…"'` is not a keyed hash of the bare card number, and on it as a URL or form encoding decodes it (`unquote_plus`, as `redact_url` and `redact_body` read it), so `%224111…%22` or `4111+1111+…` in text is the label in every pack, as a `%5BSECRET-MASKED%5D` redact_url wrote is masked already; and a value that is masking's own output once normalized (a label, token or truncation in quotes, as the credential text rule leaves `password="[SECRET-MASKED]"`) is returned as it is, never hashed into one shared token. `mask_secret(value)` (`masking/patterns.py`, exported from `ecsctx.masking` and `ecsctx` with `mask_card_value`) is that for a value outside a mapping: `None`/bools pass, anything else is masked as `str(value)`. `_mask_pii_leaf` still asks `pan_shaped` for a secret before the `pci` branch, which would truncate it. `contrib/net` masks with `mask_secret` (no `[REDACTED]` since 0.15.4): `redact_url` masks literal `secrets` in one pass, longest first (a replacement is never scanned again, so a short secret can't mangle the token a longer one became), then masks the userinfo part by part (`_mask_userinfo`, a label's brackets percent-encoded: `urlsplit` refuses brackets in a netloc whose host is not IPv6, so the result would not parse again, nor in `ecs_url`) and rewrites a credential query or fragment param in place, unencoded, leaving every other param byte for byte, and rebuilding the URL only when something changed; a query, fragment or form key the key rules call a card, CVV or SAD (`classify_key(unquote_plus(key), ALL_PACKS)`, `_card_field_type`) has its value masked by that type -- `mask_card_value`, or the label, never a token -- in `redact_url` and in `redact_body`'s form pass (`_FORM_FIELD`, run on any body with an `=`, before the credential-key pre-check); `redact_body` reads a JSON value escape-aware, past any escaped character and to the end of the text when no quote closes it (a body `json.loads` rejects is the one that reaches this rule as text), and masks what it decodes to, or the raw text when it does not decode; its closing quote is looked for, not consumed, since in malformed `""password": "…"` it also opens the next key; and a form value runs to the next `&`, whitespace or end tag (`_FORM_VALUE`), as in 0.15.3 but for the tag (every reading that ended it at a quote left the rest of some secret in clear). `redact_body` first gives every value a service's value rule matches its label (`patterns.mask_objects`, rule 2's pattern), then reads an XML element as a key (`_mask_xml_elements`): its local name, whatever its namespace prefix and attributes -- first a card, CVV or SAD name, as rule 3 reads one (`patterns.mask_card_elements`: a card object field by field, so a `` inside `` is its label), then a credential name (`_is_credential_key`, or a secret body key) as a credential, the label when it holds a card-number run -- one classification across a URL's query and an XML body, so ``, ``, `` and `` are credentials, as those query params are; the text runs to the end tag, children and all, as it decodes; a start tag no end tag closes is no element; a leaf holding JSON or XML written with entities goes through `redact_body` as it decodes and is written back escaped (only those: a form body's `&` is its separator where it stands, and decoded, a form value read differently on the next pass). `_split_form_value` keeps only the structure at its two ends as written -- a quote or an escaped one (at its head, a run of them: `password=""x`, quotes doubled as CSV escapes one, and a run of two or more of either kind, `password=''x` -- a lone `'` is the value's own, a quoted value when another closes it), and `}`, `]`, `,`, a JSON key's `":`, `>`, and `/>` after a quote (after an unquoted value a `/` right after a token reads to the credential text rule as more credential, so only `>` stays) -- and takes a label, token, truncation or placeholder followed only by structure whole (`[SECRET-MASKED],` is not `[SECRET-MASKED` and `],`). The value between them is JSON-unescaped only when it sits between escaped quotes (`\"…\"`, in a JSON string; never in a raw form body), is `[SECRET-MASKED]` if it `holds_pan_run` (asked before any pass-through: a token-shaped value can carry a card number), and otherwise goes through `mask_secret`, being written back exactly as captured when that leaves it as it is (empty, or masked already). It is decoded as form encoding reads it (`unquote_plus`: `+` is a space), so outside a form body a secret holding `+` or `%xx` gets a different token than under a key; nothing leaks. The JSON rule gives every value that `holds_pan_run` the same label, closed or run on to the end of the text, before `mask_secret` (which, without `pci`, hashes one that is not in a token's shape). When its credential passes change the text, `redact_body` runs its card/CVV/SAD field pass once more: masking a credential can uncover a field the first pass read as part of the one holding it (`"password": "password="paymentCvv=…`), so one call leaves nothing a second would mask. The text rules then `redact_body` are a fixed point (0.15.6: the scheme word's rule leaves `password=`, a quoted value stops at an end tag, and only JSON or XML leaves are decoded; on 280k generated shapes none changed on a second pass, where 0.15.5 missed 240 of 120k). ## Submodules -- `masking/` - `filters.py` (`MaskPIIFilter`, the engine), `patterns.py` (`RULES` content rules and packs, `classify_key()`, `SAFE_KEYS`, `mask_card_value`, `mask_secret`), `config.py` (pack and skip-path selection), `tokens.py` (`safe_tokenize`, `mask_by_field_type`, `already_masked`), `fields_rules.py` (`FieldRule`: tokenizable/exemptable per field type), `exemptions.py` (`configure_masking()` path-exemption state, path-prefix matching) +- `masking/` - `filters.py` (`MaskPIIFilter`, the engine), `patterns.py` (`RULES` content rules and packs, `classify_key()`, `SAFE_KEYS`, `mask_card_value`, `mask_secret`), `config.py` (pack and skip-path selection, a service's safe keys and value rules), `value_rules.py` (`ValueRule`: a service's own shapes of value, masked as their label), `tokens.py` (`safe_tokenize`, `mask_by_field_type`, `already_masked`), `fields_rules.py` (`FieldRule`: tokenizable/exemptable per field type), `exemptions.py` (`configure_masking()` path-exemption state, path-prefix matching) - `pii/` - Keyset-based PII module: `provider.py` (KeysetProvider ABC), `keyset.py` (FileKeysetProvider with hot-reload), `vault.py` (VaultKeysetProvider with AppRole auth), `crypto.py` (HMAC + AES-GCM), `normalize.py` (email/phone normalization; strips wrapping quotes + whitespace before AND after the type-specific normalizer) - `contrib/django/` - Django-specific middleware and lazy-loading processors, masking boot-check (`checks.py`) - `contrib/rq/` - RQ job context propagation (decorator-based: `@with_log_context`) @@ -28,6 +28,7 @@ Core structured logging module with ECS field mapping and PII/PCI masking. ## Footguns - `SAFE_KEYS` whitelist (`masking/patterns.py`) prevents masking of non-PII fields containing "name" etc. (e.g., `service_name`, `pathname`); checked before every other key rule. It holds only names that mean the same in every service — never a service's own vocabulary. A service lists its own in `ECSCTX_MASK_SAFE_KEYS` / `configure_masking_safe_keys()` (`masking/config.py`, resolved like the packs, passed to `classify_key` as `safe`); anything the classifier calls a card, CVV, SAD or credential is refused (`never_safe` asks `classify_key`; expiry is not refused, since nothing masks it). A listed key also frees a digits-only reference number of up to 14 digits from the content rules when the key is not PII on its own and not inside a card/credential/CVV/SAD container (15-19 digits always truncate). Ottu's list is `ecsctx.contrib.ottu.masking.SAFE_KEYS`, opted into by each service, never installed implicitly +- Value rules (`masking/value_rules.py`): a service's own shapes of value, masked as their label wherever they are logged; ecsctx names none. `ECSCTX_MASK_VALUE_RULES` / `configure_masking_value_rules()` (`masking/config.py`, resolved like the safe keys: rule objects, or dotted import paths to a rule or a collection, comma-separated in the env var; an item that is not one raises from the call, is dropped with a warning from the setting or env var, and fails the boot check). A rule has `field_type` (the label, `make_label`), `matches(mapping)` and optional `hints` (literal strings its text holds one of: text holding none is never parsed). The engine asks them about every mapping the key walk meets under any key, every JSON-text string (`_value_label`; JSON text holding one deeper under a key that would hash it is the label, `label_within`), every JSON object in text (rule 2, `_OBJECT_TEXT`: JSON, a repr, or JSON in a JSON string, three levels deep; an object no rule matches is read member by member) and every such object in a body (`redact_body` -> `patterns.mask_objects`, first). A match is the rule's label, never hashed. Nothing configured, nothing runs: the rules are looked up only when a mapping, JSON text or text holding `{` asks, at most once a call (`_Pass.values`). - CVV never goes through tokenization at all, even when PII is configured — the CVV rules return the literal `[CVV-MASKED]` directly. PCI forbids storing CVV in any form, not even as an HMAC digest. - SSN/PEM tokens are computed over a normalized value (digits-only for SSNs, base64-body-only for PEM), not the raw matched text — otherwise the same value in a different grouping/wrapping would produce a different token and break correlation. Card numbers carry no token at all: they truncate to first 6 + last 4 from 15 digits up and last 4 only below (`411111******1111`, bare, PCI DSS 3.5.1, FAQ 1091), separators stripped, independent of PII configuration. The star run is the marker: `already_masked()` and `_text_has_card_context()` both recognise that shape, since there is no wrapper left to read. The card rule (`_CardRun`) reads a whole run of digit groups at once: where a separator admits one card or a card beside another number, Luhn decides, and every Luhn-valid 12-19-digit stretch along the groups lies inside a truncated span (overlapping ones merged), so no output shows more than its first six and last four -- except over digits it leaves as something else's: an IBAN's (`_iban_end`: its country's registered length from `_IBAN_LENGTHS`, in groups of four joined by single spaces, check digits holding; after a letter bank code only if the IBAN ends where the run does), a word's own or a truncation's last four (unless they start a Luhn-valid card in card-style groups, which makes them a card's), a phone number's after "+" (at most 15 digits, E.164, even a card's), a range's across a Unicode dash (no stretch crosses it; a Unicode dash is a separator only when the whole run is card-style groups), and a canonical UUID's (`_UUID`; the run is read again without them, `_truncated_outside`). Accepted residuals follow from those: a card glued to a word or to `****` in non-card-style groups, one split by a range's dash, a card completing an IBAN of its country's exact length with check digits holding, a card typed into a UUID's own groups (`41111111-1111-1111-abcd-ef0123456789` stays whole in free text, and under a name key with a keyset it is hashed), and the phone rule (it runs first) taking phone-shaped digits at a card's end (ten before a Unicode dash, or its last digits with those after them). A later pass leaves a truncation's first six and last four alone. A card key never shows those residuals: it refuses instead, since `mask_card_value` shows its scan only while fewer than twelve digits show outside the truncations the card rule writes, however they are joined -- double spaces, commas, another masker's `X`s (`_digits_beside_truncations`, group by group: a search for the shape from every star of a long run was quadratic). Nor may twelve digits show in a row, however joined (`_card_digits_showing`, one running count): bare digits, a truncation's last four and the next one's first six count together, as a card number could be read across them; a truncation's own stars separate its first six from its last four; a word ends a row, but a run of `X`/`x` joins digits as stars do (another masker's `4508750XXX001019`). So `4111111111 111111******1111`, `400001******5018 00 000009******0001` (the Maestro 501800000009), `********4111 ********1111 1111`, `02313379 ********1262`, `411111******3782XX82246310005`, `512345******0008 12 25 512345******0008` and `4111111111111111 12345678901` are refused. The one residual left: a short group beside a truncation, under twelve digits with it (`450875******1019 000` could be a 19-digit number another masker cut oddly). - No dict-level "already masked" marker: a dict re-passed to `_mask_dict()` (e.g. `mask_sensitive_data` running twice in one chain) is fully re-traversed. Idempotency lives in the leaf checks instead — `mask_by_field_type` leaves an already-`[LABEL…]` value untouched via `already_masked()`, and a second content-regex pass over masked markers is a noop, so `_mask_string` always runs the full pass with no early-exit (a whole-string `already_masked()` gate there would let a coincidental `-MASKED]` fragment suppress masking for real PII in the same string). Only `filter()` marks anything, and only on the `LogRecord` (`mark_object_as_masked`/`is_masked_object`, a plain attribute that never ships as output), so a record reaching two handlers skips masking on the second pass. diff --git a/ecsctx/contrib/django/checks.py b/ecsctx/contrib/django/checks.py index 4020755..0abc80b 100644 --- a/ecsctx/contrib/django/checks.py +++ b/ecsctx/contrib/django/checks.py @@ -215,13 +215,18 @@ def find_masking_errors(logging_config: dict[str, Any]) -> list[str]: The entry points below all go through this, so none of them can pass while the other half is broken. """ - from ecsctx.masking.config import masking_pack_errors, masking_safe_key_errors + from ecsctx.masking.config import ( + masking_pack_errors, + masking_safe_key_errors, + masking_value_rule_errors, + ) return ( find_masking_config_errors(logging_config) + find_unmasked_live_handlers(logging_config) + masking_pack_errors() + masking_safe_key_errors() + + masking_value_rule_errors() ) diff --git a/ecsctx/contrib/net/__init__.py b/ecsctx/contrib/net/__init__.py index 949eeaa..ccbec73 100644 --- a/ecsctx/contrib/net/__init__.py +++ b/ecsctx/contrib/net/__init__.py @@ -23,6 +23,7 @@ from __future__ import annotations import contextlib +import html import json import os import re @@ -30,17 +31,24 @@ from http import HTTPStatus from typing import Any from urllib.parse import unquote_plus, urlparse, urlsplit, urlunsplit +from xml.sax.saxutils import escape as xml_escape from ecsctx.masking.filters import MaskPIIFilter from ecsctx.masking.patterns import ( - ALL_PACKS, + _XML_START, + _card_field_type, + _closing, + _element_text, + _end_tags, + _mask_card_field, _mask_userinfo, - classify_key, holds_pan_run, - mask_card_value, + mask_card_elements, + mask_objects, mask_secret, + value_rules_in_force, ) -from ecsctx.masking.tokens import _MASKED_VALUE, _PLACEHOLDER, make_label, mask_by_field_type +from ecsctx.masking.tokens import _MASKED_VALUE, _PLACEHOLDER, make_label _SECRET_LABEL = f"[{make_label('secret')}]" @@ -229,7 +237,7 @@ def _get_body_log_cap() -> int: def _get_compiled() -> tuple: - """Cached (hint_re, json_re, form_re) for the current key set.""" + """Cached (hint_re, json_re, form_re, element names) for the current key set.""" global _compiled_cache keys = _get_secret_keys() cache_key = keys @@ -256,10 +264,13 @@ def _get_compiled() -> tuple: # Only the structure at its two ends stays outside the mask # (_split_form_value). form_re = re.compile( - r"\b(" + "|".join(keys) + r")=([^&\s]*)", + r"\b(" + "|".join(keys) + r")=(" + _FORM_VALUE + ")", re.IGNORECASE, ) - _compiled_cache = (cache_key, (hint_re, json_re, form_re)) + # An XML element named by one of them, lowercased: element names are + # matched case-insensitively, as the keys are. + names = frozenset(key.lower() for key in keys) + _compiled_cache = (cache_key, (hint_re, json_re, form_re, names)) return _compiled_cache[1] @@ -268,21 +279,12 @@ def _is_credential_key(key: str) -> bool: return k in _CREDENTIAL_EXACT or any(hint in k for hint in _CREDENTIAL_HINTS) -# What a query or form key names that is masked by its type, whatever the -# value looks like: a CVV and the rest of Sensitive Authentication Data as -# their labels, never a token, and a card number truncated. Classified as the -# key walk classifies a key, every pack on: MIGS's `vpc_CardSecurityCode`, a -# form's `card_number`, `pin`. -_CARD_TYPES = frozenset({"cvv", "sad", "card"}) - - -def _card_field_type(key: str) -> str | None: - field_type = classify_key(key, ALL_PACKS) - return field_type if field_type in _CARD_TYPES else None - - -def _mask_card_field(value: str, field_type: str) -> str: - return mask_card_value(value) if field_type == "card" else mask_by_field_type(value, field_type) +# A form value: to the next `&` or whitespace, and never into an end tag. In +# `password=abc` the element's text ends there, as the +# credential text rules read it: read on, the value took the tag with it. +# Unrolled: a lookahead at every character of every value made a 64 KB form +# body's field pass twice as slow as 0.15.5's. +_FORM_VALUE = r"[^&\s<]*(?:<(?!/)[^&\s<]*)*" def redact_url(url: str, *, secrets: Collection[str] | None = None) -> str: @@ -386,18 +388,38 @@ def redact_body(text: str) -> str: unquoted -- so it carries the token the same value gets under a key; one already masked passes through. - A form value runs to the next ``&`` or whitespace. Only the structure at - its two ends -- a quote or an escaped one, and ``}``, ``]``, ``,``, a - JSON key's ``":``, ``>`` or ``/>`` -- stays as written; the rest is the - value, unescaped first only when it sits between escaped quotes in a - JSON string. A value, JSON or form, that holds a card-number run is the - label. A form field whose key the key rules call a card, CVV or other - SAD is masked by that type, whatever the credential keys: a card number - truncated, a CVV ``[CVV-MASKED]``. + A form value runs to the next ``&``, whitespace or end tag. Only the + structure at its two ends -- a quote or an escaped one, and ``}``, + ``]``, ``,``, a JSON key's ``":``, ``>`` or ``/>`` -- stays as written; + the rest is the value, unescaped first only when it sits between escaped + quotes in a JSON string. A value, JSON or form, that holds a card-number + run is the label. A form field whose key the key rules call a card, CVV + or other SAD is masked by that type, whatever the credential keys: a + card number truncated, a CVV ``[CVV-MASKED]``. + + An XML element is read as a key: its local name, whatever its namespace + prefix and attributes, classified as a query or form key is. A card, CVV + or SAD name masks its text by that type, as the text rule does + (``mask_card_elements``: a card object field by field); then a + credential name (as ``redact_url`` reads a param's, or one of the secret + keys) masks its text as a credential, the label when it holds a + card-number run, a CVV inside it already its label. The text runs to + the end tag, children and all, and is masked as it decodes (a CDATA + section's content, entities resolved). Any other element's text that is + JSON or XML written with entities goes through these rules as it + decodes, and is written back escaped. + + A value a value rule in force matches (``ECSCTX_MASK_VALUE_RULES``), as + JSON anywhere in the body or as JSON in a JSON string, is the rule's + label first, before a credential's rule could hash it. """ + if "{" in text and (rules := value_rules_in_force()): + text = mask_objects(text, rules) + if "<" in text: + text = _mask_xml_elements(text) if "=" in text: text = _FORM_FIELD.sub(_mask_card_form_field, text) - hint_re, json_re, form_re = _get_compiled() + hint_re, json_re, form_re, _names = _get_compiled() if not hint_re.search(text): return text masked = form_re.sub(_mask_form_value, json_re.sub(_mask_json_value, text)) @@ -411,8 +433,69 @@ def redact_body(text: str) -> str: # Any form field: its key as a form writes one (`card[number]` too), not -# glued to more of one, and its value up to the next `&` or whitespace. -_FORM_FIELD = re.compile(r"(? bool: + """A credential's element name: one a query param's name would be read + as (`_is_credential_key`), or one of the secret body keys.""" + return _is_credential_key(name) or name.lower() in names + + +def _mask_xml_elements(text: str) -> str: + """Each element named as a card, CVV or SAD key is, its text masked by + that type, as the text rule masks one (`mask_card_elements`) -- first, so + a CVV inside a credential's element is its label, not part of the text + the credential's token hashes. Then each element named as a credential + key is, its text masked as a credential; and entity-encoded text in any + other leaf, through the body rules as it decodes.""" + text = mask_card_elements(text) + names = _get_compiled()[3] + parts, copied, ends = [], 0, None + for start in _XML_START.finditer(text): + if start.start() < copied: + continue # inside an element already masked whole + if ends is None: + ends = _end_tags(text) + # No end tag, no element: a route (`/v1/cards//`) is a + # start tag's shape. Each end tag is found once (`_end_tags`). + if (closing := _closing(ends, start)) is None: + continue + end = closing.start() + if not _is_credential_element(start.group("local"), names): + if (masked := _encoded_leaf(text, start.end(), end)) is not None: + parts += [text[copied : start.end()], masked] + copied = end + continue + value = _element_text(text[start.end() : end]) + masked = _SECRET_LABEL if holds_pan_run(value) else mask_secret(value) + if masked != value: + parts += [text[copied : start.end()], masked] + copied = end + if not parts: + return text + parts.append(text[copied:]) + return "".join(parts) + + +def _encoded_leaf(text: str, start: int, end: int) -> str | None: + """The text of a leaf, ``text[start:end]``, masked by the body rules as + it decodes and escaped again -- when it is JSON or XML written with + entities, and masking changed it. A leaf's raw text needs nothing here, + nor a form body's: the body rules read them where they stand, `&` a + form's separator either way. Decoded, a form value ran on past `"`, + and a leaf the first pass left as a form body read differently on the + next.""" + if text.find("<", start, end) != -1 or text.find("&", start, end) == -1: + return None + decoded = html.unescape(text[start:end]) + if decoded.lstrip()[:1] not in ("{", "[", "<"): + return None + masked = redact_body(decoded) + if masked == decoded: + return None + return xml_escape(masked) def _mask_card_form_field(match: re.Match) -> str: diff --git a/ecsctx/contrib/ottu/masking.py b/ecsctx/contrib/ottu/masking.py index 880160d..a15b9a9 100644 --- a/ecsctx/contrib/ottu/masking.py +++ b/ecsctx/contrib/ottu/masking.py @@ -1,4 +1,6 @@ -"""Key names Ottu services log that are not PII, for ``ECSCTX_MASK_SAFE_KEYS``. +"""Ottu's masking vocabulary: key names Ottu services log that are not PII +(``SAFE_KEYS``, for ``ECSCTX_MASK_SAFE_KEYS``), and the wallet tokens they +carry, masked by shape (``WALLET_RULES``, for ``ECSCTX_MASK_VALUE_RULES``). ecsctx's own safe keys hold only names that mean the same in every service. These are Ottu's vocabulary, where a generic key rule would mask a value that is not @@ -12,16 +14,30 @@ so a service pinned to an older ecsctx keeps working, and because listing a key that is already safe costs nothing. -A service opts in from its settings:: +An Apple Pay or Google Pay token carries the device card number and the +payment cryptogram, encrypted: Sensitive Authentication Data, the SAD label +under any key and in any text, never hashed. Which shapes those are is Ottu's +knowledge, not ecsctx's: core names none, and masks one only as a service's +value rule (``ecsctx.masking.value_rules``). ``WALLET_RULES`` are Ottu's. - from ecsctx.contrib.ottu.masking import SAFE_KEYS as OTTU_SAFE_KEYS +A service opts in to both from its settings:: + + from ecsctx.contrib.ottu.masking import SAFE_KEYS as OTTU_SAFE_KEYS, WALLET_RULES ECSCTX_MASK_SAFE_KEYS = [*OTTU_SAFE_KEYS, "x-script-name"] + ECSCTX_MASK_VALUE_RULES = [*WALLET_RULES] + +or from the environment: +``ECSCTX_MASK_VALUE_RULES=ecsctx.contrib.ottu.masking.WALLET_RULES``. Nothing installs them implicitly: ``register_ottu()`` registers events, and what a service masks stays in its own settings. """ +from typing import Any + +from ecsctx.masking.value_rules import ValueRule + SAFE_KEYS = frozenset({ # Ottu PG API fields: a gateway's short name ("mpgs") and flags saying # whether a card payment needs its CVV. @@ -70,3 +86,30 @@ "installation_name", "tokenization_status", }) + + +# Apple Pay's PKPaymentToken `paymentData`: its `version`, and the encrypted +# `data`. Its `signature` and `header` are useless without `data`, so the whole +# object is the label, and an unrelated `paymentData` reads through. +_APPLE_PAY_VERSIONS = frozenset({"EC_v1", "RSA_v1"}) +# Google Pay's payment method token: its `protocolVersion` and the +# `signedMessage` holding the encrypted payload. +_GOOGLE_PAY_VERSIONS = frozenset({"ECv1", "ECv2", "ECv2SigningOnly"}) + + +def _is_apple_pay_token(value: Any) -> bool: + version = value.get("version") + return isinstance(version, str) and version in _APPLE_PAY_VERSIONS and "data" in value + + +def _is_google_pay_token(value: Any) -> bool: + protocol = value.get("protocolVersion") + return isinstance(protocol, str) and protocol in _GOOGLE_PAY_VERSIONS and "signedMessage" in value + + +APPLE_PAY_RULE = ValueRule("sad", _is_apple_pay_token, hints=("EC_v1", "RSA_v1")) +GOOGLE_PAY_RULE = ValueRule("sad", _is_google_pay_token, hints=("ECv1", "ECv2")) +# Found by shape, never by a key name: under `paymentData`, TAP's `token_data`, +# MPGS's `paymentToken` (a JSON string), Google Pay's `tokenizationData.token`, +# KPay's ``, or in a message. +WALLET_RULES = (APPLE_PAY_RULE, GOOGLE_PAY_RULE) diff --git a/ecsctx/events/spec.py b/ecsctx/events/spec.py index 2e53e46..1b2a68c 100644 --- a/ecsctx/events/spec.py +++ b/ecsctx/events/spec.py @@ -72,9 +72,11 @@ class EventSpec: # A `Reason` subclass, normalised to its members; a tuple of strings still # works for a set nobody passes as an object. reasons: _ReasonSet = () - # Field names this event is expected to carry. Declaration only — the - # runtime validator (#159491) is what enforces them. Declared here so - # EventSpec is not reopened for that ticket. + # Field names this event is expected to carry. Declaration only: nothing + # enforces them. The contract validator (`event_contract`, #159491) checks + # an event's action, its outcome, the level it is logged at and `labels`, + # not these fields; they are rendered into the catalogue's reference page + # ("Carries") for the developer and the reviewer. required: tuple[str, ...] = () optional: tuple[str, ...] = () # Connect's LogEvent carried only `level`, described as "the level of the diff --git a/ecsctx/masking/__init__.py b/ecsctx/masking/__init__.py index 0967070..b2bfc11 100644 --- a/ecsctx/masking/__init__.py +++ b/ecsctx/masking/__init__.py @@ -11,15 +11,18 @@ See ecsctx.masking.patterns for the rules and packs, ecsctx.masking.config for choosing packs (configure_masking_packs, ECSCTX_MASKING_PACKS), a service's own safe key names (configure_masking_safe_keys, -ECSCTX_MASK_SAFE_KEYS) and skip paths, and ecsctx.masking.exemptions for the configure_masking() -path-exemption API. +ECSCTX_MASK_SAFE_KEYS), its value rules (configure_masking_value_rules, +ECSCTX_MASK_VALUE_RULES; ecsctx.masking.value_rules) and skip paths, and +ecsctx.masking.exemptions for the configure_masking() path-exemption API. """ from ecsctx.masking.config import ( configure_masking_packs, configure_masking_safe_keys, + configure_masking_value_rules, get_masking_packs, get_masking_safe_keys, + get_masking_value_rules, key_field_type, ) from ecsctx.masking.exemptions import ( @@ -31,6 +34,7 @@ from ecsctx.masking.install import install_maskers, uninstall_maskers from ecsctx.masking.patterns import ALL_PACKS, PACK_NAMES, mask_card_value, mask_secret from ecsctx.masking.tokens import mask_by_field_type, safe_tokenize +from ecsctx.masking.value_rules import ValueRule __all__ = [ "ALL_PACKS", @@ -39,6 +43,9 @@ "get_masking_packs", "configure_masking_safe_keys", "get_masking_safe_keys", + "configure_masking_value_rules", + "get_masking_value_rules", + "ValueRule", "MaskPIIFilter", "install_maskers", "uninstall_maskers", diff --git a/ecsctx/masking/config.py b/ecsctx/masking/config.py index 170530d..d7805ef 100644 --- a/ecsctx/masking/config.py +++ b/ecsctx/masking/config.py @@ -14,7 +14,10 @@ A service's own safe key names (``ECSCTX_MASK_SAFE_KEYS``) resolve the same way: ``configure_masking_safe_keys()``, else the Django setting, else the environment variable, else none. They extend ``patterns.SAFE_KEYS``, which holds -only names that mean the same in every service. +only names that mean the same in every service. So do its value rules +(``ECSCTX_MASK_VALUE_RULES``, ``ecsctx.masking.value_rules``): rule objects or +dotted import paths, ``configure_masking_value_rules()`` first; the environment +variable names import paths, comma-separated. The Django setting is read lazily, at log time, because logging is configured while settings are still being imported. Until settings are configured the @@ -31,15 +34,19 @@ from functools import lru_cache from ecsctx.masking.patterns import ALL_PACKS, classify_key, never_safe +from ecsctx.masking.value_rules import load_value_rules _ENV_VAR = "ECSCTX_MASKING_PACKS" _SAFE_KEYS_VAR = "ECSCTX_MASK_SAFE_KEYS" +_VALUE_RULES_VAR = "ECSCTX_MASK_VALUE_RULES" _ALWAYS = frozenset({"default"}) _explicit: frozenset[str] | None = None _resolved: frozenset[str] | None = None _explicit_safe: frozenset[str] | None = None _resolved_safe: frozenset[str] | None = None +_explicit_rules: tuple | None = None +_resolved_rules: tuple | None = None _warned: set[str] = set() @@ -267,6 +274,58 @@ def get_masking_safe_keys() -> frozenset[str]: return names +# Until Django settings are configured nothing is cached, and every lookup +# reads the env var again: remember what each env value imports. +_loaded_env_rules = lru_cache(maxsize=8)(load_value_rules) + + +def configure_masking_value_rules(rules: Iterable | str | None) -> None: + """The value rules this service masks by shape, wherever they are logged + (``ecsctx.masking.value_rules``): rule objects, or dotted import paths to + a rule or to a collection of them. ``None`` goes back to settings/env. + Raises on an item that is not one: this runs while logging is being + configured, where failing loudly is right.""" + global _explicit_rules, _resolved_rules + if rules is None: + _explicit_rules = None + else: + loaded, problems = load_value_rules(rules) + if problems: + raise ValueError("; ".join(problems)) + _explicit_rules = loaded + _resolved_rules = None + + +def masking_value_rule_errors() -> list[str]: + """Items in the setting or env var that are not value rules, for the Django boot check.""" + if _explicit_rules is not None: + return [] + _ready, source, value = _configured_value(_VALUE_RULES_VAR) + _rules, problems = _loaded_env_rules(value) if isinstance(value, str) else load_value_rules(value) + return [f"{source}: {problem}. It masks nothing." for problem in problems] + + +def get_masking_value_rules() -> tuple: + """The service's value rules. Never raises: this runs on every log line. + + An item in the setting or env var that is not a rule is dropped with a + warning once, and the boot check reports it; the rest apply. + """ + global _resolved_rules + if _explicit_rules is not None: + return _explicit_rules + if _resolved_rules is not None: + return _resolved_rules + settings_ready, source, value = _configured_value(_VALUE_RULES_VAR) + rules, problems = _loaded_env_rules(value) if isinstance(value, str) else load_value_rules(value) + if problems and source not in _warned: + _warned.add(source) + warnings.warn(f"{source}: {'; '.join(problems)}. It masks nothing.", RuntimeWarning, stacklevel=2) + if settings_ready: + _resolved_rules = rules + return rules + + def key_field_type(key: str) -> str | None: """The field type the engine gives a key name, under this service's packs and safe keys -- or None for a name it leaves to the content rules. @@ -289,9 +348,11 @@ def key_field_type(key: str) -> str | None: def _reset_masking_config() -> None: """Forget every choice. For tests.""" - global _explicit, _resolved, _explicit_safe, _resolved_safe + global _explicit, _resolved, _explicit_safe, _resolved_safe, _explicit_rules, _resolved_rules _explicit = None _resolved = None _explicit_safe = None _resolved_safe = None + _explicit_rules = None + _resolved_rules = None _warned.clear() diff --git a/ecsctx/masking/filters.py b/ecsctx/masking/filters.py index cf3bbd4..3bba5d5 100644 --- a/ecsctx/masking/filters.py +++ b/ecsctx/masking/filters.py @@ -33,8 +33,11 @@ ) from ecsctx.masking.fields_rules import get_field_rule from ecsctx.masking.patterns import ( + _CRED_LITERALS, + _CVV_LITERALS, _KEY_SEPARATORS, _MIN_PAN_DIGITS, + _PAYMENT_ID_KEYWORD, _SAFE_KEYS_JOINED, ALL_PACKS, SAFE_KEYS, @@ -50,8 +53,10 @@ pan_shaped, rules_for, scalar_rules, + value_rules_in_force, ) from ecsctx.masking.tokens import make_label, mask_by_field_type +from ecsctx.masking.value_rules import label_of, label_within, text_label _IS_MASKED_ = "_IS_MASKED_" @@ -133,6 +138,19 @@ class _Pass(NamedTuple): rules: tuple exempt: tuple safe: frozenset[str] # the service's own safe keys (ECSCTX_MASK_SAFE_KEYS) + # The value rules in force (ECSCTX_MASK_VALUE_RULES), looked up the first + # time a mapping or JSON text asks, at most once a call: `[None]` until + # then. None looks them up at every ask. + values: list | None = None + + +def _value_rules(ctx: _Pass) -> tuple: + cell = ctx.values + if cell is None: + return value_rules_in_force() + if cell[0] is None: + cell[0] = value_rules_in_force() + return cell[0] # Walked though not exemptable: a card object, and a credential, CVV or SAD key @@ -265,11 +283,53 @@ def _pair(data: dict, ctx: _Pass, inherited: str | None) -> tuple[str | None, fr # What a record becomes when masking itself fails: never the unmasked text. MASKING_FAILED = "[MASKING-FAILED: {}]" -# A printf-style conversion, as the % operator reads one. +# A printf-style conversion, as CPython's % operator reads one: `%%`, a +# literal percent that takes no argument, or a mapping key -- to the +# parenthesis that balances its first, as CPython counts them -- then any +# flags, a width (`*` or digits), a precision (`.` then `*` or digits, none +# at all included: `%.f` is `%.0f`), one length modifier, which is ignored, +# and the conversion. A `%` after anything but the first `%` is no +# conversion: CPython refuses it. _CONVERSION = re.compile( - r"%(?:\((?P[^)]*)\))?(?P[#0\- +]*)(?P\*|\d+)?(?:\.(?P\*|\d+))?[hlL]?" - r"(?P[diouxXeEfFgGcrsa%])" + r"%(?:%|(?:\((?P(?:[^()]|\((?:[^()]|\([^()]*\))*\))*)\))?(?P[#0\- +]*)(?P\*|\d+)?" + r"(?:\.(?P\*|\d*))?[hlL]?(?P[diouxXeEfFgGcrsa]))" +) + + +# A word a content rule keys a value on, before a placeholder -- a +# credential's or an auth scheme's, a CVV's, a card's, a payment id's -- as +# the rules' pre-checks spell them. +_JOINING_WORD = re.compile( + "|".join((*map(re.escape, _CRED_LITERALS), *map(re.escape, _CVV_LITERALS), "card", r"\bpan\b", _PAYMENT_ID_KEYWORD)), + re.IGNORECASE, ) +# How far before a placeholder its key word may stand: `the cvv is %s`. +_JOINING_REACH = 64 +# A placeholder between plain words: no text before it, or a whole word of +# letters and a space; and no text after it, or sentence punctuation, then a +# space and a whole word of letters. +_PLAIN_BEFORE = re.compile(r"(?:\A|(? bool: + """Whether rendering ``template`` can put what a content rule reads + across a placeholder, which neither the template nor an argument holds on + its own: `cvv=%s` with `123`, `card %s %s` with a card number and its CVV. + Not where each placeholder stands between plain words, no key word within + reach before it (`user %s paid %s KWD`). Cached: a service logs a fixed + set of templates.""" + placeholders = [found for found in _CONVERSION.finditer(template) if found["type"] is not None] + for index, found in enumerate(placeholders): + start = placeholders[index - 1].end() if index else 0 + end = placeholders[index + 1].start() if index + 1 < len(placeholders) else len(template) + before, after = template[start : found.start()], template[found.end() : end] + if (index and not before) or not _PLAIN_BEFORE.search(before) or not _PLAIN_AFTER.match(after): + return True + if _JOINING_WORD.search(template, max(0, found.start() - _JOINING_REACH), found.start()): + return True + return False def _template_of(msg: Any) -> str | None: @@ -286,12 +346,12 @@ def _template_of(msg: Any) -> str | None: return str(msg) -def _renders(template: str, args: Any) -> bool: +def _render(template: str, args: Any) -> str | None: + """``template % args``, as getMessage() renders it; None where it raises.""" try: - template % args + return template % args except (TypeError, ValueError, KeyError): - return False - return True + return None def _became_text(original: Any, masked: Any) -> bool: @@ -320,7 +380,7 @@ def _as_masked_text(template: str, originals: Any, masked: Any) -> str: return template parts, copied, index = [], 0, 0 for found in _CONVERSION.finditer(template): - if found["type"] == "%": + if found["type"] is None: # `%%` takes no argument continue # A `*` width or precision takes an argument of its own first. index += (found["width"] == "*") + (found["precision"] == "*") @@ -437,6 +497,25 @@ def _mask_pii_leaf(text: str, field_type: str, ctx: _Pass) -> str: return mask_by_field_type(text, field_type) +# What JSON text can start with: nearly every string starts with something +# else, and no value rule is asked about it. +_JSON_STARTS = frozenset("{[ \t\r\n") + + +def _value_label(value: Any, ctx: _Pass) -> str | None: + """The label of a value a value rule in force matches, as a mapping or as + JSON text (value_rules): under any key, a credential's or a card's + included, never hashed, never shown.""" + kind = type(value) + if kind is dict: + rules = _value_rules(ctx) + return label_of(value, rules) if rules else None + if kind is str and value[:1] in _JSON_STARTS: + rules = _value_rules(ctx) + return text_label(value, rules) if rules else None + return None + + def _mask_card_list_element(value: Any) -> Any: """A bare value in a list under a card key -- `card=(pan, month, cvv)`. @@ -494,7 +573,7 @@ def _context(self) -> _Pass: packs in force (config.call_packs), so the card check mask_by_field_type makes follows them as well as the process's.""" packs = self._packs_in_force() - return _Pass(packs, rules_for(packs), _get_exempt_patterns(), get_masking_safe_keys()) + return _Pass(packs, rules_for(packs), _get_exempt_patterns(), get_masking_safe_keys(), [None]) def _mask_string(self, text: str, ctx: _Pass | None = None, *, scalar: bool = False) -> str: # No already_masked() early-exit here: that helper is a whole-string @@ -541,6 +620,11 @@ def _mask_dict( if path == () and key in self._skip_keys: result[key] = self._mask_skipped(key, value, ctx) continue + if (label := _value_label(value, ctx)) is not None: + # Under any key, a credential's or a card's included: never + # hashed, never shown. + result[key] = label + continue if isinstance(value, bool): # One bit: never PII, SAD or a credential, whatever its key or # container. Masking it only destroyed the flag. @@ -623,6 +707,15 @@ def _mask_dict( # Never below a card, credential, CVV or SAD container: a broad # exempt prefix must not expose `…token.name_on_card`. result[key] = self._mask_value(value, child_path, ctx) + elif ( + isinstance(value, str) + and value[:1] in _JSON_STARTS + and (rules := _value_rules(ctx)) + and (label := label_within(value, rules)) is not None + ): + # JSON text holding a value a rule matches deeper, under a key + # that would hash it whole or show it: the label. + result[key] = label elif field_type == "card" and not isinstance(value, _CONTAINERS): result[key] = mask_card_value(value) elif (field_rule.exemptable or field_type in _WALKED_TYPES) and isinstance( @@ -704,9 +797,16 @@ def _mask_value( if verbatim_digits and _is_reference_number(value): return value # The two exact types nearly every value is, first: this runs for - # every value of every record. + # every value of every record. Each is a value rule's label before + # anything else reads it (`_value_label`, spelled out here). kind = type(value) if kind is str: + if ( + value[:1] in _JSON_STARTS + and (rules := _value_rules(ctx)) + and (label := text_label(value, rules)) is not None + ): + return label if inherited == "card": return _mask_card_list_element(value) if inherited is not None: @@ -717,6 +817,8 @@ def _mask_value( # is a field value, and its key has already had its say. return self._mask_string(value, ctx, scalar=path != ()) if kind is dict: + if (rules := _value_rules(ctx)) and (label := label_of(value, rules)) is not None: + return label return self._mask_dict(value, path, ctx, inherited) if isinstance(value, dict): return self._mask_dict(value, path, ctx, inherited) @@ -842,17 +944,30 @@ def filter(self, record: logging.LogRecord) -> bool: msg = self._mask_value(record.msg, (), ctx) args = self._mask_args(record.args, ctx) template = _template_of(record.msg) - if record.args and template is not None and (msg != template or not _renders(msg, args)): + if ( + record.args + and template is not None + and ( + msg != template + or (rendered := _render(msg, args)) is None + or (_may_join(template) and self._mask_value(rendered, (), ctx) != rendered) + ) + ): # getMessage() would raise on what masking left, so the # handler dropped the line and printed the arguments to # stderr: the template held something to mask (in # `password=%s` the rule reads the placeholder as the # credential), a number masking turned into text meets - # a `%d`, or the arguments never fit. So the record is + # a `%d`, or the arguments never fit. Or it renders a + # value split between the template and an argument + # (`cvv=%s` with `123`), which neither holds on its own: + # what a handler that never calls format() printed, + # and Sentry and handleError read. So the record is # rendered from the masked arguments (a dict's keys # still mask it) and masked whole, or is the marker. A - # template masking leaves as it is, and that renders, - # keeps its arguments, and Sentry's grouping by it. + # record whose masked arguments render to text masking + # leaves as it is keeps its template and arguments, + # and Sentry's grouping by them. # Rendered, the record keeps no arguments for a later # handler to mask with more packs, so they are masked # with every pack: a default handler ahead of a pci one diff --git a/ecsctx/masking/install.py b/ecsctx/masking/install.py index fae0599..508beaf 100644 --- a/ecsctx/masking/install.py +++ b/ecsctx/masking/install.py @@ -8,11 +8,12 @@ besides the formatter: Sentry's logging integration, Handler.handleError, handlers that never call format(). Those readers render the record themselves: a template masking leaves as it is keeps its arguments, each -masked on its own, and nothing masks the line they make. A value split -between template and argument (`cvv=%s` with `123`) reaches them as -`cvv=123`; only the formatter's own pass (mask_sensitive_data) masks the -formatted line whole. Call it before dictConfig() so the handlers dictConfig -builds carry the filter from the start. +masked on its own, when the line they render holds nothing more to mask. A +value split between template and argument (`cvv=%s` with `123`) makes a line +that does, so that record is rendered and masked whole, its arguments `()`: +they read `cvv=[CVV-MASKED]`, as the formatter's own pass +(mask_sensitive_data) writes it. Call it before dictConfig() so the handlers +dictConfig builds carry the filter from the start. install_maskers_on_handlers() sweeps any handler that already exists as a live object right now (third-party handlers, hand-built ones, or a logging diff --git a/ecsctx/masking/patterns.py b/ecsctx/masking/patterns.py index bf98df3..2effd8f 100644 --- a/ecsctx/masking/patterns.py +++ b/ecsctx/masking/patterns.py @@ -3,7 +3,7 @@ Ported from ottu_pg's MaskPIIFilter (utils/log/filters.py), merged with ecsctx's own PII key-name list. Two independent detection strategies: -1. Content-based (RULES): 18 ordered regexes in three packs — `default` +1. Content-based (RULES): 24 ordered rules in three packs — `default` always on, `pci` and `financial_ids` opt-in (ecsctx.masking.config) — each behind a literal pre-check, applied to every string the filter reaches. Rule order is load-bearing — see the comments on each rule and the @@ -25,14 +25,18 @@ from __future__ import annotations +import ast import contextlib +import html import json import re +from bisect import bisect_left from collections import deque from collections.abc import Callable, Iterator from functools import lru_cache from heapq import merge from itertools import accumulate, pairwise +from operator import methodcaller from typing import Any, NamedTuple from urllib.parse import unquote @@ -43,9 +47,11 @@ _TOKEN_SHAPE, _TOKEN_START, _TRUNCATED_PAN, + already_masked, make_label, mask_by_field_type, ) +from ecsctx.masking.value_rules import hinted, label_of # --------------------------------------------------------------------------- # Shared keyword/value fragments @@ -130,7 +136,7 @@ }) # Authorization / Authorisation (+ _header). Apart from the other keywords: -# its value is a scheme and a credential, one value (rule 3). +# its value is a scheme and a credential, one value (rule 7). _AUTH_KEYWORD = r"authori[sz]ation(?:[_-]?header)?" # The HTTP authentication schemes an Authorization header's credential # follows (RFC 9110 11.4), and the one Ottu's API names `Api-Key`. @@ -203,13 +209,17 @@ # ends it, so it would cut the label (`[SECRET-MASKED]]`), and a label that # redact_body wrote after a short value made it long enough to mask again. _NOT_A_LABEL = r"(?!(?-i:\[[A-Z0-9-]+-MASKED[\]:]))" +# ...nor into an end tag: in `password=abc` the element's text, +# and so the value, ends there, as a form value in redact_body does. Read on, +# the value took the tag with it, and the next pass hashed its token again. +_NOT_AN_END_TAG = r"(?!` (`` in a JSON string). -_INNER_QUOTES = rf"(?:\\?[\"'])+(?!:|/>)(?={_NOT_A_LABEL}(?:{_PLAIN}|{_ESCAPED}))" -_VALUE_UNIT = rf"(?:{_NOT_A_LABEL}(?:{_PLAIN}|{_ESCAPED}|{_INNER_QUOTES}))" +_INNER_QUOTES = rf"(?:\\?[\"'])+(?!:|/>)(?={_NOT_A_LABEL}{_NOT_AN_END_TAG}(?:{_PLAIN}|{_ESCAPED}))" +_VALUE_UNIT = rf"(?:{_NOT_A_LABEL}{_NOT_AN_END_TAG}(?:{_PLAIN}|{_ESCAPED}|{_INNER_QUOTES}))" # A value's first character: never a quote, which is the structure around a # value, nor an opening bracket: `{` and `[` open a container (JSON text the # key walk re-serialised), `[` also a label masking already wrote. @@ -236,6 +246,14 @@ def _quoted_body(quote: str) -> str: return rf"(?:(?!(?P={quote}))[^\\]|\\[\s\S])" +def _quoted_value_body(quote: str) -> str: + """...for a value after `key=`: never into an end tag, as an unquoted one + and redact_body's form value are not. Read across one, the value that + redact_body left before a tag (`token=">"`) was hashed + again.""" + return rf"(?:(?!(?P={quote})| str: # tokenize. Stated directly rather than via mask_by_field_type('', 'cvv'), # which made these rules depend on how an empty value is rendered. _CVV_LABEL = f"[{make_label('cvv')}]" +_SECRET_LABEL = f"[{make_label('secret')}]" @@ -343,7 +362,7 @@ def _truncate_pan(digits: str) -> str: _DIGIT_VALUE = bytes.maketrans(b"0123456789", bytes(range(10))) _DOUBLED_VALUE = bytes.maketrans(b"0123456789", bytes((0, 2, 4, 6, 8, 1, 3, 5, 7, 9))) # An IBAN's country code and two check digits, at the start of a word, as the -# first two digits of a run. Case-sensitive, as rule 11 is. +# first two digits of a run. Case-sensitive, as rule 16 is. _IBAN_HEAD = re.compile(rf"(? str: return f"{_mask_url_part(user)}{separator}{_mask_url_part(password)}" +# --------------------------------------------------------------------------- +# Value rules in text +# --------------------------------------------------------------------------- +# A JSON object written in text -- JSON, a Python repr, or JSON in a JSON +# string -- is asked of the service's value rules (value_rules), and one a +# rule matches is that rule's label, never a token. ecsctx names no shape of +# its own: nothing configured, nothing runs. + + +def value_rules_in_force() -> tuple: + """The value rules masking asks: the service's (config's + ``ECSCTX_MASK_VALUE_RULES``). Imported here: config imports this module + as it loads.""" + from ecsctx.masking.config import get_masking_value_rules + + return get_masking_value_rules() + + +def _json_object(string: str, space: str, character: str) -> str: + """A JSON object as text: members whose value is a string, a scalar, or + an object or list holding strings, anything but a bracket, and one more + level of either -- as deep as an object a rule is asked about goes; a + deeper one is read from inside. Every alternative starts on a character + of its own and no loop takes a closing bracket, so a failed match never + backtracks into another reading.""" + flat = rf"(?:{string}|{character})*" + nested = rf"(?:{string}|{character}|\{{{flat}\}}|\[{flat}\])*" + value = rf"(?:{string}|[-+.\w]+|\{{{nested}\}}|\[{nested}\])" + member = rf"{string}{space}:{space}{value}" + return rf"\{{{space}{member}(?:{space},{space}{member})*{space}\}}" + + +# As written: JSON, or a Python repr's quotes. Escaped: JSON in a JSON string +# (a field whose value is itself JSON), its quotes `\"`, a pretty-printed +# one's line breaks `\n`. Compiled once, as rule 2. +_OBJECT_TEXT = ( + "(?P" + + _json_object( + r"(?:\"(?:[^\"\\]|\\[\s\S])*\"|'(?:[^'\\]|\\[\s\S])*')", + r"\s*", + r"[^{}\[\]\"']", + ) + + ")|(?P" + + _json_object( + rf'\\"{_ESCAPED_QUOTED_UNIT}*\\"', + r"(?:\s|\\[nrt])*", + r'(?!\\")[^{}\[\]"]', + ) + + ")" +) + + +def _object_value(m: re.Match) -> Any: + """The object a match wrote, parsed: JSON, a Python repr, or JSON in a + JSON string as it decodes. None where it does not parse.""" + written = m.group(0) + if m.group("plain") is None: + try: + return json.loads(json.loads(f'"{written}"')) + except (ValueError, RecursionError): + return None + try: + return json.loads(written) + except (ValueError, RecursionError): + pass + try: + return ast.literal_eval(written) + except (ValueError, TypeError, SyntaxError, MemoryError, RecursionError): + return None + + +def _mask_object(m: re.Match, rules: tuple) -> str: + """The label where an object a rule matches stood: a JSON string where it + was a JSON value (after `:`, `,` or `[`), escaped where that JSON is + itself in a string; bare anywhere else -- in prose, an element's text, a + string of its own. An object no rule matches is read member by member, + so one inside it is still found.""" + written = m.group(0) + label = label_of(_object_value(m), rules) + if label is None: + inner = mask_objects(written[1:-1], rules) + return written if inner == written[1:-1] else f"{{{inner}}}" + text, position = m.string, m.start() - 1 + while position >= 0 and text[position].isspace(): + position -= 1 + if position < 0 or text[position] not in ":,[": + return label + quote = '"' if m.group("plain") is not None else '\\"' + return f"{quote}{label}{quote}" + + +def _value_object(m: re.Match) -> str: + rules = value_rules_in_force() + return _mask_object(m, rules) if rules else m.group(0) + + +def mask_objects(text: str, rules: tuple) -> str: + """Each JSON object written in ``text`` that one of ``rules`` matches, as + its label: rule 2 alone, for ``contrib.net.redact_body``.""" + if not rules or "{" not in text or not hinted(text, rules): + return text + return _OBJECT_RULE.pattern.sub(lambda m: _mask_object(m, rules), text) + + def _mask_pem(match: re.Match) -> str: """Token computed over the base64 body only — strip the BEGIN/END lines and all whitespace first, so the same key re-wrapped at a different line @@ -1140,11 +1263,293 @@ def _cred_kv(m: re.Match) -> str: return f"{prefix}{_mask_credential(val, keyword)}" +# A container after a credential word, as a %-style argument renders one +# (`password=['a', 'b']`, `{'password': ('a', 'b')}`): a list, dict, tuple or +# set, its strings read whole so a bracket in one ends nothing, nested three +# deep; or, opened and never closed (a line cut short), the rest of the line. +_CONTAINER_STRING = r"\"(?:[^\"\\]|\\[\s\S])*\"|'(?:[^'\\]|\\[\s\S])*'" + + +def _container(depth: int) -> str: + inner = rf"(?:{_CONTAINER_STRING}|[^\[\]{{}}()\"']" + (f"|{_container(depth - 1)}" if depth else "") + ")*" + return rf"(?:\[{inner}\]|\{{{inner}\}}|\({inner}\))" + + +_CONTAINER = rf"(?:{_container(2)}|[\[{{(][^\n]*)" + + +def _only_masks(value: Any) -> bool: + """Whether every value in a parsed container is masking's own output -- + a token, a label, a truncation -- or no value at all (None, a flag).""" + if value is None or isinstance(value, bool): + return True + if isinstance(value, str): + return already_masked(value) + if isinstance(value, (list, tuple, set, frozenset)): + return all(_only_masks(item) for item in value) + if isinstance(value, dict): + return all(_only_masks(item) for item in value.values()) + return False + + +def _masked_already(text: str) -> bool: + """Whether a container's text holds only masking's own output, as the key + walk writes a credential's container, a record's field included.""" + for parse in (ast.literal_eval, json.loads): + try: + return _only_masks(parse(text)) + except (ValueError, TypeError, SyntaxError, MemoryError, RecursionError): + continue + return False + + +def _cred_container(m: re.Match) -> str: + value = m.group("value") + if _masked_already(value): + return m.group(0) + # A repr's key is quoted: its value stays a string. + quote = m.group("quote") + return f"{m.group('prefix')}{quote}{_mask_credential(value, m.group('key'))}{quote}" + + +# Masking's own output in a value: a whole token, a label, a truncation. +_MASKS_IN_A_VALUE = re.compile(rf"(?-i:{_CARDLESS_TOKEN}|\[[A-Z0-9-]+-MASKED\]|{_TRUNCATED_PAN})") +_DIGIT = re.compile(r"\d") +# A form field whose value masking wrote, as redact_body leaves one after a +# scheme word (`API-Key password=`). +_MASKED_FIELD = re.compile(rf"[\w.\-\[\]%]+[=:](?-i:{_CARDLESS_TOKEN}|\[[A-Z0-9-]+-MASKED\])") + + +def _api_key_scheme(m: re.Match) -> str: + """`API-Key ` without its `Authorization:` -- Ottu PG's header, + written into text: the scheme and the key are one value, as after + Authorization, and carry the token the header carries. A form field + masking wrote there (`API-Key password=`) is left, never hashed + again with the scheme.""" + if _MASKED_FIELD.fullmatch(m.group("value")): + return m.group(0) + return mask_secret(m.group(0)) + + def _cred_space(m: re.Match) -> str: - kw, val = m.group(1), m.group(2) + kw, val = m.group("keyword"), m.group("value") + if _DIGIT.search(_MASKS_IN_A_VALUE.sub("", val)) is None: + # Its only digits are masking's own: `password=`, which + # redact_body wrote after the scheme word. The digit is what makes + # this rule read a credential, and hashing it again was a token of a + # token. A credential glued to a key name keeps its own digits + # (`Token abc123abc123api_key=`), and is hashed as before. + return m.group(0) return f"{kw} {_mask_credential(val, kw)}" +# An element's text given as a CDATA section, whose content is the value. +_CDATA = re.compile(r"\s*[\s\S]*?)\]\]>\s*") +# An Authorization element's text that masking already wrote, or another +# masker did: a scheme before a whole token, a label or a placeholder. As in +# rule 7, the scheme is not hashed again with it. +_SCHEME_BEFORE_A_MASK = re.compile( + rf"\s*{_AUTH_SCHEME}[ \t]+(?:(?-i:{_CARDLESS_TOKEN})|\[[^\[\]]*\]|\*+)\s*", re.IGNORECASE +) +_AUTH_NAME = re.compile(_AUTH_KEYWORD, re.IGNORECASE) + + +def _element_text(raw: str) -> str: + """An XML element's text as it decodes: a CDATA section's content, or the + text with its entities resolved -- the value a key would carry, so it + gets the token the same value gets under a key.""" + if (cdata := _CDATA.fullmatch(raw)) is not None: + return cdata.group("content") + return html.unescape(raw) if "&" in raw else raw + + +# A start tag's attributes: a `>` inside a quoted value is the value's, as +# XML allows it there (``); read to the first `>`, the +# rest of the value went into the element's text, and its token. Each piece +# starts with a character no other can (no backtracking to speak of), and a +# quoted value never crosses a `<`. +_ATTRIBUTES = r"(?:[^<>\"']|\"[^\"<]*\"|'[^'<]*')*" +# An XML element's start tag: its qualified name, the local name after any +# namespace prefix, and its attributes. Not an empty-element tag +# (``), which holds no text. A quote nothing closes before the +# next tag leaves the first `>` to end it, as before: never no start tag, +# which would leave the element's text in clear. +_XML_START = re.compile( + rf"<(?P(?:[A-Za-z_][\w.-]*:)?(?P[A-Za-z_][\w.-]*))" + rf"(?:(?:\s{_ATTRIBUTES})?(?|(?:\s[^<>]*)?(?)" +) +# ...and an end tag, with its qualified name. +_XML_END = re.compile(r"(?:[A-Za-z_][\w.-]*:)?[A-Za-z_][\w.-]*)\s*>") +_match_start = methodcaller("start") + + +def _tag_key(tag: str) -> str: + """A tag's name as an end tag is paired with a start tag: ignoring case, + as the rules read a name.""" + return tag.translate(_ASCII_FOLDS).casefold() + + +def _end_tags(text: str) -> dict[str, list[re.Match]]: + """The end tags in ``text``, in order, by name (`_tag_key`).""" + found: dict[str, list[re.Match]] = {} + for end in _XML_END.finditer(text): + found.setdefault(_tag_key(end.group("tag")), []).append(end) + return found + + +def _closing(ends: dict[str, list[re.Match]], start: re.Match) -> re.Match | None: + """The end tag that closes the element ``start`` opens -- the first of its + name after it -- in ``ends`` (`_end_tags`), or None.""" + found = ends.get(_tag_key(start.group("tag"))) + if not found: + return None + at = bisect_left(found, start.end(), key=_match_start) + return found[at] if at < len(found) else None + + +def _mask_elements(text: str, kind: Callable[[str], str | None], mask: Callable[[str, str], str]) -> str: + """``text`` with each element's text masked as ``mask(text, kind)`` masks + it, where ``kind`` gives the element's local name a kind (None: not one + to mask). An element runs to the first end tag of its name after its + start tag, children and all, and is read whole: none inside it is looked + for. No end tag, no element: a route in a message + (`DELETE /v1/cards//`) is a start tag's shape. + + The text's end tags are found once, when the first element of a kind + needs one: looked for again from each start tag, a text holding many + that nothing closes took quadratic time -- 160 KB of ``, + twelve seconds. + """ + parts, copied, after, ends = [], 0, 0, None + for start in _XML_START.finditer(text): + if start.start() < after: + continue + if (found := kind(start.group("local"))) is None: + continue + if ends is None: + ends = _end_tags(text) + if (closing := _closing(ends, start)) is None: + continue + after = closing.end() + raw = text[start.end() : closing.start()] + masked = mask(raw, found) + if masked != raw: + parts += [text[copied : start.end()], masked] + copied = closing.start() + if not parts: + return text + parts.append(text[copied:]) + return "".join(parts) + + +_CRED_NAME = re.compile(_CRED_KEYWORD, re.IGNORECASE) + + +def _credential_name(local: str) -> str | None: + return local if _CRED_NAME.fullmatch(local) else None + + +def _mask_credential_element(raw: str, keyword: str) -> str: + value = _element_text(raw) + if _AUTH_NAME.fullmatch(keyword) and _SCHEME_BEFORE_A_MASK.fullmatch(value): + return raw + masked = _mask_credential(value, keyword) + return raw if masked == value else masked + + +def _cred_elements(m: re.Match) -> str: + return _mask_elements(m.group(0), _credential_name, _mask_credential_element) + + +# What a key names that is masked by its type, whatever the value looks like: +# a CVV and the rest of Sensitive Authentication Data as their labels, never a +# token, and a card number truncated. Classified as the key walk classifies a +# key, every pack on: MIGS's `vpc_CardSecurityCode`, a form's `card_number`, +# `pin`. A query param, a form field and an XML element are read so +# (contrib.net, rule 3). +_CARD_TYPES = frozenset({"cvv", "sad", "card"}) +# ...and those whose element is its label whole, children and all. +_LABEL_TYPES = frozenset({"cvv", "sad"}) + + +def _card_field_type(key: str) -> str | None: + field_type = classify_key(key, ALL_PACKS) + return field_type if field_type in _CARD_TYPES else None + + +def _label_field_type(key: str) -> str | None: + field_type = classify_key(key, ALL_PACKS) + return field_type if field_type in _LABEL_TYPES else None + + +def _mask_card_field(value: str, field_type: str) -> str: + return mask_card_value(value) if field_type == "card" else mask_by_field_type(value, field_type) + + +def mask_card_elements(text: str) -> str: + """``text`` with each XML element whose local name the key rules call a + card, a CVV or other SAD masked as that key's value is, in every pack -- + rule 3, and redact_body's first pass over a body's elements: + + - a CVV's or SAD's text, children and all, is its label; + - a card's text is a card value (`mask_card_value`): a card number + truncated, what a card key refuses `[CARD-MASKED]`, anything else + shown. A card element holding elements -- a card object, CyberSource's + `` -- is read as the key walk reads one: a CVV or SAD element in + it is its label, and every other element's text a card value, so a + card number under `` is truncated as under + ``, and an expiry reads through. + + A text is masked as it decodes (a CDATA section's content, entities + resolved), and masking's own output passes through. + """ + return _mask_elements(text, _card_field_type, _mask_card_element) + + +def _mask_card_element(raw: str, field_type: str) -> str: + if field_type != "card": + value = _element_text(raw) + masked = mask_by_field_type(value, field_type) + return raw if masked == value else masked + if "<" not in raw: + return _card_run(raw) + raw = _mask_elements(raw, _label_field_type, _mask_card_element) + # Each CDATA section's content, and each run of text between the markup, + # a card value. A section is found by `find`, not a lazy regex, which + # read on to the end of the text from each opening nothing closes. + parts, at = [], 0 + while (opening := raw.find("", opening + 9)) == -1: + break + parts.append(_TEXT_RUN.sub(_card_text, raw[at:opening])) + content = raw[opening + 9 : closing] + masked = mask_card_value(content) + parts.append(raw[opening : closing + 3] if masked == content else masked) + at = closing + 3 + parts.append(_TEXT_RUN.sub(_card_text, raw[at:])) + return "".join(parts) + + +# Markup -- a quoted attribute's `>` its own -- or a run of text between it. +_TEXT_RUN = re.compile(rf"<{_ATTRIBUTES}>|<[^<>]*>|(?P[^<]+)") + + +def _card_text(m: re.Match) -> str: + return m.group(0) if m.group("text") is None else _card_run(m.group(0)) + + +def _card_run(text: str) -> str: + """A run of a card element's text, masked as a card value as it + decodes.""" + value = html.unescape(text) if "&" in text else text + masked = mask_card_value(value) + return text if masked == value else masked + + +def _card_elements(m: re.Match) -> str: + return mask_card_elements(m.group(0)) + + def _cvv_quoted(m: re.Match) -> str: q, kw, sep = m.group("q"), m.group("key"), m.group("sep") return f"{q}{kw}{q}{sep}{q}{_CVV_LABEL}{q}" @@ -1159,8 +1564,21 @@ def _cvv_space(m: re.Match) -> str: return f"{m.group(1)} {_CVV_LABEL}" -def _standalone_cvv(_m: re.Match) -> str: - return _CVV_LABEL +def _cvv_beside_card(m: re.Match) -> str: + """Each field of the card's that is three or four digits alone, as the + label: which of `1227` and `123` after a card is its CVV no rule can + tell, so neither ships. A date (`12/27`), a field its word names as the + expiry (`exp 1227`) and a month or year of one or two digits stay.""" + return m.group("card") + _CARD_FIELD.sub(_cvv_field, m.group("fields")) + + +def _cvv_field(field: re.Match) -> str: + value = field.group("field") + return field.group("separator") + (_CVV_LABEL if _BARE_CVV.fullmatch(value) else value) + + +def _cvv_after_word(m: re.Match) -> str: + return f"{m.group('lead')}{_CVV_LABEL}" def _payid_quoted(m: re.Match) -> str: @@ -1187,7 +1605,16 @@ def _phone(m: re.Match) -> str: def _userinfo(m: re.Match) -> str: - return f"{m.group(1)}{_mask_userinfo(m.group(2))}@" + scheme, userinfo = m.group("scheme"), m.group("userinfo") + if m.group("user") is not None: + # A user alone is an account (`ssh://git@host`) -- unless it is shaped + # like a card number or holds a card-number run: a saved card's + # gateway token, as redact_url reads it, and never its hash. + user = unquote(userinfo) + if not (pan_shaped(user) or holds_pan_run(user)): + return m.group(0) + return f"{scheme}{_SECRET_LABEL}@" + return f"{scheme}{_mask_userinfo(userinfo)}@" # How far before an email's local part a URL's scheme can end when what is @@ -1252,9 +1679,6 @@ class Rule(NamedTuple): # Replaces pattern.sub for rules whose matches can only start at a few # positions it can find cheaply; same output as pattern.sub. scan: Callable[[re.Pattern, Callable, str], str] | None = None - # Unlike `gate`, this one DOES change the result: it says whether the rule - # applies to this text at all. Only the standalone-CVV rule has one. - precondition: Callable[[str, str], bool] | None = None # True for a rule that may only run over prose -- a human message, a # serialised body -- and never over a whole scalar field value. A field # value has a key to be judged by; applying a keyless shape rule to it @@ -1292,6 +1716,22 @@ def _has_credential(_text: str, lowered: str) -> bool: return any(word in lowered for word in _CRED_LITERALS) +def _has_value_object(text: str, _lowered: str) -> bool: + return "{" in text and bool(rules := value_rules_in_force()) and hinted(text, rules) + + +def _has_credential_container(text: str, lowered: str) -> bool: + return ("[" in text or "{" in text or "(" in text) and _has_credential(text, lowered) + + +def _has_credential_element(text: str, lowered: str) -> bool: + return "<" in text and _has_credential(text, lowered) + + +def _has_end_tag(text: str, _lowered: str) -> bool: + return " bool: return any(word in lowered for word in _CVV_LITERALS) or _CARD_CODE.search(lowered) is not None @@ -1329,41 +1769,42 @@ def _has_ssn_shape(text: str, _lowered: str) -> bool: return _SSN_SHAPE.search(text) is not None -def _has_three_digits(text: str, _lowered: str) -> bool: - return _THREE_DIGITS.search(text) is not None - - -# Rules 16 and 17 have already run by the time rule 18 does, so a PAN in the -# text is now a bare truncation rather than a digit run. _TRUNCATED_PAN_RE -# below is what recognises it; these words cover a "card"/"pan" key name -# serialised into the text, and the prose cases. -# "cvv", "cvc" and "security" too: text that says "cvv" anywhere is card -# context even when the keyword rules cannot reach the digits ("the cvv is -# 123" -- rule 9 needs them adjacent). -_CARD_CONTEXT = ("card", "pan", "cardholder", "credit", "cvv", "cvc", "security") -_TRUNCATED_PAN_RE = re.compile(_TRUNCATED_PAN) - - -def _text_has_card_context(text: str, lowered: str) -> bool: - """Whether this text holds anything a CVV could belong to. - - A 3-4 digit group with no card anywhere near it is a status code, a count - or an amount, and a CVV is worth nothing without its PAN. Rules 16 and 17 - have already run, so a PAN is a bare truncation by now -- the star run is - what recognises it, as the words cover a card-named key in the text. - """ - if any(word in lowered for word in _CARD_CONTEXT): - return True - # A PAN the card rule has already truncated: rule 16 runs first, so by now - # the digit run _CARD_SHAPE looks for is gone and the truncation is the - # only card context left in the text. Without this, a CVV sitting beside a - # masked PAN stops being masked -- which is a leak, not a formatting bug. - return _CARD_SHAPE.search(text) is not None or _TRUNCATED_PAN_RE.search(text) is not None +def _has_card_and_digits(text: str, _lowered: str) -> bool: + # A truncation's stars, or a card number's digits, and three digits. + return _THREE_DIGITS.search(text) is not None and ("*" in text or _CARD_SHAPE.search(text) is not None) + + +# A bare CVV's card: a card number the card rule truncated -- its first six, +# stars and last four, or stars and the last four -- or a run of 12-19 digits +# it left as it is (one glued to a word). The card rule runs first, so a PAN +# in the text is its truncation by the time the bare CVV rules look. +_CVV_CARD = r"(?:(?{_CVV_SEPARATOR})(?P{_CVV_FIELD})", re.IGNORECASE) +# A field that may be the CVV: three or four digits, nothing else. +_BARE_CVV = re.compile(r"\d{3,4}") +# The CVV after its word: three or four digits, as a field is. +_CVV_DIGITS = r"\d{3,4}(?![\w*/-]|[.:]\d)" +# Between a CVV word and its digits: a sign, or a word or two a sentence puts +# there (`the cvv is 123`, `security code was 1234`, `cvv number 123`). +_CVV_LINK = r"(?=[\s:=#-])(?:\s+(?:is|was|of|number|value|code)){0,2}\s*[:=#-]?\s*" # Every credential match contains one of these words, and starts inside the # run of [\w-] characters holding it, or on the quote right before that run -# (rule 2's quoted key). So the credential rules only need trying at those +# (rule 5's quoted key). So the credential rules only need trying at those # positions, not at every position of a long body — re.sub tries every one, # and at ~16 µs per rule per gateway body that was most of the masking cost. # The words are found with str.find on the lowercased text: a case-insensitive @@ -1401,10 +1842,10 @@ def _cvv_word_starts(lowered: str) -> list[int]: # A credential match starts at most this far before its credential word: the -# bounded key prefix (128), its separator, and rule 2's opening quote. +# bounded key prefix (128), its separator, and rule 5's opening quote. _CRED_REACH = 130 # ...and a CVV match before its CVV word: the bounded key prefix (64) and its -# separator, or a camelCase word (64), `card_`, and rule 4's opening quote. +# separator, or a camelCase word (64), `card_`, and rule 9's opening quote. _CVV_REACH = 72 @@ -1461,12 +1902,12 @@ def scan(pattern: re.Pattern, repl, text: str) -> str: _sub_near_cvv_words = _near_words(_cvv_word_starts, _CVV_REACH) -def _rule(pack, regex, repl, gate, scan=None, prose_only=False, precondition=None): - return (pack, regex, repl, gate, scan, prose_only, precondition) +def _rule(pack, regex, repl, gate, scan=None, prose_only=False): + return (pack, regex, repl, gate, scan, prose_only) # --------------------------------------------------------------------------- -# The 18 content rules, in execution order. DO NOT REORDER — several rules +# The 24 content rules, in execution order. DO NOT REORDER — several rules # only behave correctly because a more specific rule ran first: # 1. Credential/CVV/payment-id keyword rules before all shape rules — # otherwise a numeric secret in the card-digit range gets masked as a @@ -1478,14 +1919,26 @@ def _rule(pack, regex, repl, gate, scan=None, prose_only=False, precondition=Non # 4. Phone before the card rule — same reason. # 5. Email before card/CVV/SSN — a numeric-heavy address must mask as one # email rather than fragment. -# 6. SSN before standalone-CVV — a space-separated SSN's outer groups are -# each individually CVV-standalone-shaped. -# 7. Standalone-CVV last — it is the loosest rule in the file (any bare -# 3-4 digit group); widening its boundary set beyond whitespace/string- -# boundary over-masks. +# 6. SSN before the bare CVV rules — a space-separated SSN's outer groups +# are each CVV-shaped. +# 7. The bare CVV rules last — the card a CVV sits beside is the +# truncation the card rule wrote. # 8. URL userinfo before phone and email — a password is a credential, # not a phone number, and `user:pw@host.tld` keeps its host rather than # masking as an email. +# 9. The XML credential element before the other credential rules — they +# would mask a `password=`, `"token": …` or `Bearer x` inside its text +# first, and the element's value, holding their token, was hashed again. +# 10. The value rule before every credential rule, the XML one included — +# a value a rule matches under `"token": …` or in `` was +# hashed whole. +# 11. The `API-Key` scheme before the `key=value` credential rule — that +# would mask a `password=` inside its value first, which it never does +# after `Authorization:`, and the two forms would carry different tokens. +# 12. The XML card, CVV and SAD element before the XML credential element — +# a `` inside `` was part of the text the credential's +# token hashed, where the key walk gives a CVV under a credential key +# its label. # --------------------------------------------------------------------------- _RULE_TABLE = ( @@ -1496,7 +1949,49 @@ def _rule(pack, regex, repl, gate, scan=None, prose_only=False, precondition=Non _mask_pem, _has_pem, ), - # 2. Credential — quoted key ("token": "abc123"), the value to its + # 2. Value rules — a JSON object in text, or in a JSON string, that a + # value rule in force matches (`value_rules`, ECSCTX_MASK_VALUE_RULES): + # the rule's label, never a token, in every pack. Nothing configured, + # nothing runs; text holding none of the rules' hints is never parsed. + # Before the credential rules, which would hash one under `"token": …` + # first. + _rule( + "default", + _OBJECT_TEXT, + _value_object, + _has_value_object, + ), + # 3. Card, CVV and SAD — an XML element named as the key walk names one + # (123, …, + # , ), whatever namespace prefix and attributes it has, + # its text masked as that key's value is (`mask_card_elements`): a CVV's + # or SAD's its label, children and all; a card's a card value, and a card + # object's field by field. In every pack, as the key walk reads a key: a + # test card that fails Luhn under is truncated without + # `pci`. No end tag, no element, as for rule 4, and the same walk. + _rule( + "default", + r"<[\s\S]*", + _card_elements, + _has_end_tag, + ), + # 4. Credential — an XML element (s3cret, + # …), for a body that + # never went through redact_body: its local name a credential keyword, as + # rules 5 and 7 read a key, whatever namespace prefix and attributes it + # has. The value runs to the end tag, children and all, and is masked as + # it decodes (`_element_text`). No end tag, no element: a route in a + # message (`DELETE /v1/cards//`) is a start tag's shape. The + # walk is the rule (`_mask_elements`): the pattern hands it the text from + # the first `<`, since a regex pairing each start tag with its end tag + # looked for one from every start tag, in quadratic time. + _rule( + "default", + r"<[\s\S]*", + _cred_elements, + _has_credential_element, + ), + # 5. Credential — quoted key ("token": "abc123"), the value to its # unescaped closing quote. _rule( "default", @@ -1506,7 +2001,21 @@ def _rule(pack, regex, repl, gate, scan=None, prose_only=False, precondition=Non _has_credential, _sub_near_credential_words, ), - # 3. Credential — ":" / "=" (secret_key=abc123). A value that opens with a + # 6. Credential — Ottu's `API-Key` scheme word standing alone (not the end + # of a header name, `X-API-Key`), without its `Authorization:`: the + # scheme and the value are one value, as after Authorization (rule 7), and + # carry the header's token. Any value to its delimiter -- the word is the + # evidence, and a key may have no digit (1 in 1,550 of Ottu PG's Fernet + # keys). Before rule 7, which would mask a `password=` inside the value + # first, as it never does after `Authorization:`. + _rule( + "default", + rf"(?{_UNQUOTED_VALUE})", + _api_key_scheme, + _has_credential, + _sub_near_credential_words, + ), + # 7. Credential — ":" / "=" (secret_key=abc123). A value that opens with a # quote a closing one matches runs to it (`quote`); any other, to its # delimiter. An empty quoted value is none: quotes doubled as CSV and SQL # escape one (`password=""s3cret`) are structure before the value. The @@ -1521,16 +2030,31 @@ def _rule(pack, regex, repl, gate, scan=None, prose_only=False, precondition=Non _rule( "default", rf"\b(?P(?P(?P{_AUTH_KEYWORD})|{_OTHER_CRED_KEYWORD})(?:\\?[\"']|\s)*[:=]\s*" - rf"(?:(?P(?(?\\\")(?={_ESCAPED_QUOTED_UNIT}+\\\")|(?:\\?[\"'])+)?)" - rf"(?P(?(quote){_quoted_body('quote')}+|(?(escaped){_ESCAPED_QUOTED_UNIT}+|(?!{_WHOLE_TOKEN})" + rf"(?P(?(quote){_quoted_value_body('quote')}+|(?(escaped){_ESCAPED_QUOTED_UNIT}+|(?!{_WHOLE_TOKEN})" rf"(?(auth)(?:{_AUTH_SCHEME}[ \t]+(?={_VALUE_START})(?!{_WHOLE_TOKEN}))?(?!{_AUTH_SCHEME}[ \t]+\S))" rf"{_UNQUOTED_VALUE})))", _cred_kv, _has_credential, _sub_near_credential_words, ), - # 4. CVV — quoted key ("cvv": "123"). The keyed CVV rules (4, 5, 9) are + # 8. Credential — a container after the key (`password=['a', 'b']`), as a + # %-style argument renders one: rule 7 never starts a value at an opening + # bracket, so `password=%(pw)s` with a list shipped it whole (#160054). + # The whole container is the value, and a repr's quoted key keeps it a + # string. Not a JSON key's: the key walk masked that container by its keys + # before writing it (the saved card under `token` shows its own number, + # brand and expiry); nor a container holding only masking's own output, + # which a record's field renders. + _rule( + "default", + rf"\b(?P(?P{_CRED_KEYWORD})(?P'?)\s*[:=]\s*){_NOT_A_LABEL}(?P{_CONTAINER})", + _cred_container, + _has_credential_container, + _sub_near_credential_words, + ), + # 9. CVV — quoted key ("cvv": "123"). The keyed CVV rules (9, 10, 14) are # `default`: a CVV must not ship from any service, and a default-pack one # (Connect) receives the CVV a saved-card payment sends. A value that # starts as a CVV runs to its end, as a credential's does: none of it is @@ -1542,7 +2066,7 @@ def _rule(pack, regex, repl, gate, scan=None, prose_only=False, precondition=Non _has_cvv_keyword, _sub_near_cvv_words, ), - # 5. CVV — ":" / "=" (cvv=123). + # 10. CVV — ":" / "=" (cvv=123). _rule( "default", rf"\b({_CVV_KEYWORD}(?:\\?[\"']|\s)*[:=](?:\\?[\"']|\s)*)\d{{3}}{_VALUE_UNIT}*", @@ -1550,32 +2074,32 @@ def _rule(pack, regex, repl, gate, scan=None, prose_only=False, precondition=Non _has_cvv_keyword, _sub_near_cvv_words, ), - # 6. Payment/transaction/auth id — quoted key ("payment_id": "abc12345"). + # 11. Payment/transaction/auth id — quoted key ("payment_id": "abc12345"). _rule( "financial_ids", rf"([\"'])({_PAYMENT_ID_KEYWORD})\1(\s*:\s*)\1([A-Za-z0-9_\-]+)\1", _payid_quoted, _has_id, ), - # 7. Payment/transaction/auth id (payment_id: abc12345). + # 12. Payment/transaction/auth id (payment_id: abc12345). _rule( "financial_ids", rf"\b({_PAYMENT_ID_KEYWORD}\s*[:=]\s*)([A-Za-z0-9_\-]{{8,}})\b", _payid_kv, _has_id, ), - # 8. Credential — bare space (Bearer abc12345). A value of eight or more + # 13. Credential — bare space (Bearer abc12345). A value of eight or more # characters with a digit among them, "ptok:" counted too (Bearer - # ptok:hunter2), to its delimiter. + # ptok:hunter2), to its delimiter: prose says "Bearer of bad news". _rule( "default", - rf"\b({_CRED_KEYWORD})\s+(?!{_WHOLE_TOKEN})(?=(?:(?!\d){_VALUE_UNIT})*\d)" - rf"({_VALUE_START}{_VALUE_UNIT}{{7,}})", + rf"\b(?P{_CRED_KEYWORD})\s+(?!{_WHOLE_TOKEN})(?=(?:(?!\d){_VALUE_UNIT})*\d)" + rf"(?P{_VALUE_START}{_VALUE_UNIT}{{7,}})", _cred_space, _has_credential, _sub_near_credential_words, ), - # 9. CVV — bare space (CVV 123). + # 14. CVV — bare space (CVV 123). _rule( "default", rf"\b({_CVV_KEYWORD})\s+\d{{3}}{_VALUE_UNIT}*", @@ -1583,7 +2107,7 @@ def _rule(pack, regex, repl, gate, scan=None, prose_only=False, precondition=Non _has_cvv_keyword, _sub_near_cvv_words, ), - # 10. Payment/transaction/auth id — bare space (payment_id abc12345). + # 15. Payment/transaction/auth id — bare space (payment_id abc12345). _rule( "financial_ids", @@ -1591,27 +2115,31 @@ def _rule(pack, regex, repl, gate, scan=None, prose_only=False, precondition=Non _payid_space, _has_id, ), - # 11. IBAN (GB33BUKB20201555555555). + # 16. IBAN (GB33BUKB20201555555555). _rule( "financial_ids", rf"(?-i:\b(?:{_IBAN_PREFIX})\d{{2}}[A-Z0-9]{{11,30}}\b)", _iban, _has_iban_shape, ), - # 12. URL userinfo (postgresql://user:password@host), each part masked as + # 17. URL userinfo (postgresql://user:password@host), each part masked as # redact_url masks it, the scheme, host and port kept: a DSN in exception - # text. Only with a password, empty or not (`https://key:@host`), or a - # token alone, as masking left a user: `ssh://git@host` names an account. - # Bounded: a scheme is at most 32 characters and starts no longer run of - # scheme characters, so a long run is read once. + # text. With a password, empty or not (`https://key:@host`), or a token + # alone, as masking left a user. A user alone (`user`) names an account + # (`ssh://git@host`) and is left to the other rules -- unless it is shaped + # like a card number or holds a card-number run, which is the label, as + # redact_url gives it (#160054). Bounded: a scheme is at most 32 + # characters and starts no longer run of scheme characters, so a long run + # is read once. _rule( "default", - rf"(?]*:{_USERINFO_PART}*)@", + rf"(?[A-Za-z][A-Za-z0-9+.\-]{{0,31}}://)" + rf"(?P(?-i:{_TOKEN})(?::{_USERINFO_PART}*)?|[^\s/?#:\"'<>]*:{_USERINFO_PART}*" + rf"|(?P[^\s/?#:\"'<>@]+))@", _userinfo, _has_userinfo, ), - # 13. Phone — international E.164-style or a bare local number. Union of + # 18. Phone — international E.164-style or a bare local number. Union of # ecsctx's and the ported filter's patterns: dash/space/dot all count as # separators, since real phone numbers appear with all three and neither # source pattern alone caught every real case. @@ -1625,7 +2153,7 @@ def _rule(pack, regex, repl, gate, scan=None, prose_only=False, precondition=Non _phone, _has_phone_shape, ), - # 14. Email (user@example.com). + # 19. Email (user@example.com). _rule( "default", # Bounded by RFC 5321's limits (64-char local part, 255-char domain): @@ -1635,14 +2163,14 @@ def _rule(pack, regex, repl, gate, scan=None, prose_only=False, precondition=Non _email, _has_at, ), - # 15. JWT (eyJhbGciOi....). + # 20. JWT (eyJhbGciOi....). _rule( "default", r"\beyJ[A-Za-z0-9_\-]{5,}\.[A-Za-z0-9_\-]{3,}\.[A-Za-z0-9_\-]{3,}", _jwt, _has_jwt_prefix, ), - # 16. Card number — truncated, bare (411111******1111; last 4 + # 21. Card number — truncated, bare (411111******1111; last 4 # only below 15 digits, see _truncate_pan); the middle never survives, # starred or not. A whole run of digit groups is read at once, and Luhn # decides between its readings: _CardRun. @@ -1655,38 +2183,52 @@ def _rule(pack, regex, repl, gate, scan=None, prose_only=False, precondition=Non _mask_card_run, _has_card_shape, ), - # 17. SSN (123-45-6789). + # 22. SSN (123-45-6789). _rule( "financial_ids", r"(?=\d)" + _CARD_LEAD_GUARD + r"\d{3}[-\s]?\d{2}[-\s]?\d{4}\b", _ssn, _has_ssn_shape, ), - # 18. CVV standalone — bare 3-4 digit group, no keyword (call 123 now). - # The loosest rule in the file, and now doubly fenced. It never sees a - # whole scalar field value (prose_only): there, "000" is a PSP response - # code, not a CVV, and the key says which. In prose it fires only when the - # same text carries card context, because a 3-4 digit group with no card - # anywhere near it is a status, a count or an amount -- and a CVV is worth - # nothing without the PAN it belongs to. A keyword-anchored CVV is already - # rules 4, 5 and 9's job, whatever else the text holds. + # 23. CVV beside a card — a bare 3-4 digit group among the fields written + # after a card number or its truncation, up to three of them: + # `4111111111111111 123`, `411111******1111|12/27|123`. A CVV is worth + # nothing without its card and is written beside it; any other 3-4 digit + # group in a line that mentions a card is a status, a duration, a count, + # an amount or a decline code (#160054: `card list returned 200 in 350 + # ms`). It never sees a whole scalar field value (prose_only): there, + # "000" is a PSP response code, and the key says what it is. + _rule( + "pci", + rf"(?P{_CVV_CARD})(?P(?:{_CVV_SEPARATOR}{_CVV_FIELD}){{1,3}})", + _cvv_beside_card, + _has_card_and_digits, + prose_only=True, + ), + # 24. CVV after its word — a bare 3-4 digit group after a CVV word, a + # sign or a word or two between them (`the cvv is 123`), which the keyed + # rules 9, 10 and 14 do not read. Tried near the CVV words, as they are. _rule( "pci", - r"(?:^|(?<=\s))\d{3,4}(?=\s|$)", - _standalone_cvv, - _has_three_digits, + rf"\b(?P{_CVV_KEYWORD}{_CVV_LINK}){_CVV_DIGITS}", + _cvv_after_word, + _has_cvv_keyword, + _sub_near_cvv_words, prose_only=True, - precondition=_text_has_card_context, ), ) RULES: tuple[Rule, ...] = tuple( - Rule(f"{index}:{repl.__name__}", pack, re.compile(regex, re.IGNORECASE), repl, gate, scan, pre, prose) - for index, (pack, regex, repl, gate, scan, prose, pre) in enumerate(_RULE_TABLE, start=1) + Rule(f"{index}:{repl.__name__}", pack, re.compile(regex, re.IGNORECASE), repl, gate, scan, prose) + for index, (pack, regex, repl, gate, scan, prose) in enumerate(_RULE_TABLE, start=1) ) +# The value rule, for mask_objects. +_OBJECT_RULE = next(rule for rule in RULES if rule.repl is _value_object) + + # Just the card rule, for mask_card_value: the value already has a card key # saying what it is, so the other rules have nothing to add and applying them # would mask by shape inside a field that is not about them. @@ -1695,14 +2237,36 @@ def _rule(pack, regex, repl, gate, scan=None, prose_only=False, precondition=Non ) -@lru_cache(maxsize=64) +def _by_identity(cache: dict, rules: tuple, build: Callable[[tuple], Any]) -> Any: + """What ``build`` makes of ``rules``, cached on the tuple's identity. + + Never on its hash: a tuple of rules hashes every rule, and a compiled + pattern hashes its whole program, on every call -- 30 µs a string for + the default pack once a large rule was in it, which lru_cache paid on + each lookup. Keeping ``rules`` in the entry keeps its id from being + reused; a cache past its bound is emptied, not evicted, as _clean is. + """ + entry = cache.get(id(rules)) + if entry is None or entry[0] is not rules: + if len(cache) >= _BY_IDENTITY_LIMIT: + cache.clear() + entry = cache[id(rules)] = (rules, build(rules)) + return entry[1] + + +# More distinct rule sets than a process makes: rules_for() holds one per +# combination of packs, and scalar_rules() one per set. +_BY_IDENTITY_LIMIT = 64 +_scalar: dict[int, tuple[tuple, tuple]] = {} + + def scalar_rules(rules: tuple[Rule, ...]) -> tuple[Rule, ...]: """``rules`` minus the ones that may only run over prose. - Cached on the rule tuple so the result is a stable object: mask_by_patterns - keys its known-clean set on tuple identity. + Cached on the rule tuple's identity so the result is a stable object: + mask_by_patterns keys its known-clean set on tuple identity. """ - return tuple(rule for rule in rules if not rule.prose_only) + return _by_identity(_scalar, rules, lambda rules: tuple(rule for rule in rules if not rule.prose_only)) @lru_cache(maxsize=16) @@ -1754,8 +2318,8 @@ def _clean_set(rules: tuple) -> dict[str, None]: def _mask_once(text: str, rules: tuple[Rule, ...]) -> str: lowered = _folded_lower(text) if lowered is None: - # No case-folded text to judge a precondition by, so every rule runs: - # masking more than necessary is the safe direction to fail in. + # No case-folded text to gate by, so every rule runs: masking more + # than necessary is the safe direction to fail in. for rule in rules: text = rule.pattern.sub(rule.repl, text) return text @@ -1793,9 +2357,11 @@ def mask_by_patterns(text: str, rules: tuple[Rule, ...]) -> str: return text -@lru_cache(maxsize=16) +_gates: dict[int, tuple[tuple, tuple]] = {} + + def _distinct_gates(rules: tuple[Rule, ...]) -> tuple: - return tuple(dict.fromkeys(rule.gate for rule in rules)) + return _by_identity(_gates, rules, lambda rules: tuple(dict.fromkeys(rule.gate for rule in rules))) def _passing_gates(text: str, lowered: str, gates: tuple) -> set: @@ -1818,12 +2384,6 @@ def _mask_gated(text: str, lowered: str, rules: tuple[Rule, ...]) -> str: verdict = verdicts[rule.gate] = rule.gate(text, lowered) if not verdict: continue - # Re-asked on every version of the text, like a gate: masking a PAN - # replaces it with a bare truncation, which still reads as card context - # rather than removing it, so a later pass can only become more - # permissive — never less. - if rule.precondition is not None and not rule.precondition(text, lowered): - continue if rule.scan is not None: masked = rule.scan(rule.pattern, rule.repl, text) else: @@ -1928,7 +2488,7 @@ def _is_tel_key(words: list[str]) -> bool: "", "1", "2", "3", "one", "two", "three", "data", "equivalent", "equivalentdata", "image", "raw", "stripe", }) -# A wallet's payment cryptogram and a 3DS authentication value: one-time values +# A tokenized card's payment cryptogram and a 3DS authentication value: one-time values # that authenticate a transaction, which nothing reads in a log. Matched at the # END of the key, so a verdict about one (`cavvResponseCode`) is not one. _SAD_KEY_ENDING = re.compile(r"(?:cryptogram|cavv|tavv|aav|ucaf(?:authenticationdata)?)(?:value|data)?$") diff --git a/ecsctx/masking/tokens.py b/ecsctx/masking/tokens.py index 9962284..70a7506 100644 --- a/ecsctx/masking/tokens.py +++ b/ecsctx/masking/tokens.py @@ -94,13 +94,19 @@ def already_masked(text: str) -> bool: # The types judged here for a card number inside the value, with the `pci` -# pack: every path a credential or a payment id takes into a log ends in -# mask_by_field_type, as the key walk's own `pci` branch judges a PII leaf -# (filters._mask_pii_leaf). -_PAN_CHECKED_TYPES = frozenset({"secret", "payment_id"}) +# pack: every path a credential, a payment id, an email or a phone number +# takes into a log ends in mask_by_field_type, as the key walk's own `pci` +# branch judges a PII leaf (filters._mask_pii_leaf). The email and phone text +# rules hashed one whatever it held: `4111111111111111@example.com`, +# `+4111111111111111`. +_PAN_CHECKED_TYPES = frozenset({"secret", "payment_id", "email", "phone"}) # A truncation the card rule writes, its first six and last four showing. As a # credential it is ten digits of a saved card's sixteen-digit gateway token. _SHOWN_TRUNCATION = re.compile(r"\d{6}\*{4,}\d{4}") +# ...and any truncation after an auth scheme (`Bearer 411111******1111`), +# the scheme word as a placeholder takes one: hashed whole, it was a token +# of the scheme and the truncation, where the truncation alone is the label. +_TRUNCATION_AFTER_A_SCHEME = re.compile(rf"[A-Za-z][\w-]*\s+{_TRUNCATED_PAN}") def mask_by_field_type(value: str, field_type: str) -> str: @@ -120,10 +126,13 @@ def mask_by_field_type(value: str, field_type: str) -> str: is written and as a URL or form encoding decodes it (``%22…%22``, ``+``). With `pci` among the call's packs, and the process's -- a filter's own ``packs=`` for the call, with the process's (``config.packs_in_force``) - -- a credential or payment id that holds a card-number run anywhere - (``holds_pan_run``) is the label too. Applied here, where every caller - passes -- the key walk, the credential and payment-id text rules, a - route parameter, ``ecsctx.contrib.net`` -- rather than by each of them. + -- a credential, payment id, email or phone number that holds a + card-number run anywhere (``holds_pan_run``) is the label too, a phone + number read as a phone field reads one: a "+"-led run of up to E.164's + 15 digits is the number itself. Applied here, where every caller passes + -- the key walk, the credential, payment-id, email and phone text rules, + a route parameter, ``ecsctx.contrib.net`` -- rather than by each of + them. """ field_rule = get_field_rule(field_type) label = make_label(field_rule.field_type) @@ -140,13 +149,24 @@ def mask_by_field_type(value: str, field_type: str) -> str: # encoding decodes it, as redact_url and redact_body read it before # they mask: `%224111…%22` in text was a keyed hash of a card number # in quotes, `4111+1111+…` of one with its spaces encoded. - forms = [normalize_value(value, field_type)] - if "%" in value or "+" in value: - forms.append(normalize_value(unquote_plus(value), field_type)) - if field_type == "secret" and any(_SHOWN_TRUNCATION.fullmatch(bare) for bare in forms): + if field_type == "phone": + # As written, as the key walk reads a phone field: a phone + # number's "+" is its country code, not an encoded space, and + # decoded or joined up with an extension a real number read as + # a card-number run. + forms = [value] + else: + forms = [normalize_value(value, field_type)] + if "%" in value or "+" in value: + forms.append(normalize_value(unquote_plus(value), field_type)) + if field_type == "secret" and any( + _SHOWN_TRUNCATION.fullmatch(bare) or _TRUNCATION_AFTER_A_SCHEME.fullmatch(bare) for bare in forms + ): # Never passed through as masking's own output: rendered from its # argument, a gateway token the card rule truncated showed ten - # of its digits where the key walk gives the label. + # of its digits where the key walk gives the label. After a + # scheme (`Authorization: Bearer `) it is the label + # too, never a token of the two. return f"[{label}]" if already_masked(value): return f"[{label}]" if _holds_a_card_number(value) else value @@ -164,7 +184,7 @@ def mask_by_field_type(value: str, field_type: str) -> str: return f"[{label}]" if holds_pan_run(bare) else value if pan_shaped(bare) or _PLACEHOLDER.fullmatch(bare): return f"[{label}]" - if _pci_in_force() and any(holds_pan_run(bare) for bare in forms): + if _pci_in_force() and any(holds_pan_run(bare, phone=field_type == "phone") for bare in forms): # Without `pci` among the call's packs and the process's it is # hashed: a default-pack service receives no card numbers, and # the label would stand in for 7% of the 64-hex signatures diff --git a/ecsctx/masking/value_rules.py b/ecsctx/masking/value_rules.py new file mode 100644 index 0000000..8f9464c --- /dev/null +++ b/ecsctx/masking/value_rules.py @@ -0,0 +1,155 @@ +"""Value rules: shapes of value a service masks as their label, wherever they +are logged (``ECSCTX_MASK_VALUE_RULES``). + +The key rules read a value by the name it sits under. Some values are known by +their shape instead, under whatever key and in whatever text, and which shapes +those are is a service's to say: ecsctx names none. A service lists its rules +in ``ECSCTX_MASK_VALUE_RULES`` (``ecsctx.masking.config``; +``ecsctx.contrib.ottu.masking`` has Ottu's), and the engine asks them about +every mapping the key walk meets, every string that is JSON text, and every +JSON object written in free text or in a body ``redact_body`` masks. With none +configured, none of this runs. + +A rule is anything with: + +- ``field_type``: the type whose label a matching value becomes, as + ``make_label`` spells it (``"sad"`` is ``[SAD-MASKED]``). A matching value is + never hashed, and the label is masking's own output, left as it is. +- ``matches(value)``: whether a mapping is one -- a dict the key walk meets, or + JSON text the engine parsed once to ask. +- ``hints``, optionally: literal strings a matching value's text holds at least + one of. Text holding none is never parsed to ask; without hints every JSON + object in text is. + +``ValueRule`` is one, ready made. +""" + +from __future__ import annotations + +import importlib +import json +from collections.abc import Callable, Iterable, Mapping +from dataclasses import dataclass +from typing import Any + +from ecsctx.masking.tokens import make_label + + +@dataclass(frozen=True) +class ValueRule: + """A shape of value that is its label wherever it is logged.""" + + field_type: str + matches: Callable[[Mapping[str, Any]], bool] + hints: tuple[str, ...] = () + + +def is_value_rule(rule: Any) -> bool: + field_type = getattr(rule, "field_type", None) + return isinstance(field_type, str) and bool(field_type) and callable(getattr(rule, "matches", None)) + + +def load_value_rules(value: Iterable[Any] | str) -> tuple[tuple[Any, ...], tuple[str, ...]]: + """(rules, problems): the rules a setting names -- rule objects, or dotted + import paths to a rule or to a collection of them, comma-separated in one + string as an environment variable gives them -- and what is wrong with + each item that is not one. An item that fails leaves the others.""" + if isinstance(value, str): + items: list[Any] = [part.strip() for part in value.split(",") if part.strip()] + elif isinstance(value, Iterable) and not isinstance(value, (bytes, Mapping)): + items = list(value) + else: + return (), (f"{value!r} is not a list of value rules or of their import paths",) + rules: list[Any] = [] + problems: list[str] = [] + for item in items: + try: + rules.extend(_rules_in(item)) + except ValueError as error: + problems.append(str(error)) + return tuple(rules), tuple(problems) + + +def _rules_in(item: Any) -> tuple[Any, ...]: + found = _imported(item) if isinstance(item, str) else item + if is_value_rule(found): + return (found,) + if isinstance(found, Iterable) and not isinstance(found, (str, bytes, Mapping)): + members = tuple(found) + if all(is_value_rule(member) for member in members): + return members + raise ValueError(f"{item!r} is not a value rule or a collection of them: a rule has a `field_type` and `matches(value)`") + + +def _imported(path: str) -> Any: + module, _, name = path.rpartition(".") + if not module: + raise ValueError(f"{path!r} is not a dotted import path") + try: + return getattr(importlib.import_module(module), name) + except (ImportError, AttributeError) as error: + raise ValueError(f"{path!r} does not import: {error}") from error + + +def label_of(value: Any, rules: tuple) -> str | None: + """The label of the first rule a mapping matches; None for anything else.""" + if isinstance(value, dict): + for rule in rules: + if rule.matches(value): + return f"[{make_label(rule.field_type)}]" + return None + + +def hinted(text: str, rules: tuple) -> bool: + """Whether a rule could match something in ``text``: one without hints, + or one of whose hints ``text`` holds.""" + for rule in rules: + hints = getattr(rule, "hints", ()) + if not hints or any(hint in text for hint in hints): + return True + return False + + +def parsed_json(text: str, rules: tuple) -> Any: + """``text`` parsed, when it is JSON text a rule could match; None + otherwise. Cheap for every other string: most do not start with a + bracket.""" + first = text[:1] + if first.isspace(): + first = text.lstrip()[:1] + if first not in ("{", "[") or not hinted(text, rules): + return None + try: + return json.loads(text) + except (ValueError, RecursionError): + return None + + +def text_label(text: str, rules: tuple) -> str | None: + """The label of a string that is JSON text of a value a rule matches.""" + return label_of(parsed_json(text, rules), rules) + + +_MAX_DEPTH = 64 + + +def label_within(value: Any, rules: tuple, depth: int = 0) -> str | None: + """The label of a value a rule matches anywhere in ``value`` -- a + mapping, a list, or JSON text, however deep -- or None.""" + if depth > _MAX_DEPTH: + return None + if isinstance(value, dict): + if (label := label_of(value, rules)) is not None: + return label + items = value.values() + elif isinstance(value, (list, tuple)): + items = value + elif isinstance(value, str): + found = parsed_json(value, rules) + return None if found is None else label_within(found, rules, depth + 1) + else: + return None + for item in items: + if (label := label_within(item, rules, depth + 1)) is not None: + return label + return None diff --git a/tests/golden/masking_0156.json b/tests/golden/masking_0156.json new file mode 100644 index 0000000..2b32bc3 --- /dev/null +++ b/tests/golden/masking_0156.json @@ -0,0 +1,10580 @@ +{ +"keyset/default/events":[ +{ +"event":"api request received: POST /b/checkout/v1/pymt-txn/", +"http":{ +"request":{ +"body":"{\"session_id\": \"21a2496f8b6c193ec61b8dd52cbe8d827b4eaa49\", \"order_no\": \"ORD-2026-000123\", \"amount\": \"12.500\", \"currency_code\": \"KWD\", \"customer_email\": \"ptok:v1:eMwFkFHAafEM2yUlbFIkhBKSUhHqDcNpxE3bLyWFI54\", \"customer_phone\": \"ptok:v1:kVcmxuM7xSNaFxT-GvmeIwsb_FQMPsR72OBNUiZEAQc\", \"customer_first_name\": \"ptok:v1:NVSAuFYU9wWRKK_cQt9wbZTw0HOXuh5NgHiMxgv64o8\", \"billing_address\": {\"line1\": \"ptok:v1:Ts0ReoBNKiJBYHXTW1WbXX7iUnDW6VwUwCUjR_LLNNY\", \"city\": \"ptok:v1:fKrE4cXxHekZk4mklS_6gF9X89UW19yWC8H9AaftrcE\", \"country\": \"ptok:v1:9hw5Sp-i6dZADYPdeIHwEOV3awFcLyE-i2AgY5JGTGs\"}, \"pg_codes\": [\"mpgs-direct\", \"cs-direct\", \"kpay\"], \"redirect_url\": \"https://merchant.example/return?ref=abc123&lang=en\", \"webhook_url\": \"https://merchant.example/hooks/ottu\", \"extra\": {\"note\": \"gift wrap\", \"items\": [{\"sku\": \"SKU-0\", \"qty\": 0}, {\"sku\": \"SKU-1\", \"qty\": 1}, {\"sku\": \"SKU-2\", \"qty\": 2}, {\"sku\": \"SKU-3\", \"qty\": 3}, {\"sku\": \"SKU-4\", \"qty\": 4}, {\"sku\": \"SKU-5\", \"qty\": 5}, {\"sku\": \"SKU-6\", \"qty\": 6}, {\"sku\": \"SKU-7\", \"qty\": 7}, {\"sku\": \"SKU-8\", \"qty\": 8}, {\"sku\": \"SKU-9\", \"qty\": 9}]}, \"card_acceptance_criteria\": {\"min_expiry_time\": 30}, \"three_ds\": {\"directory_server_transaction_id\": \"3d85c5d3-8412-4f1d-9e26-4a0f00123456\"}}", +"headers":{ +"authorization":"ptok:v1:s1-vTKcgCkGB8g8AIHMP2e9PJegX0mQGHGVg1OC2-UI", +"user-agent":"Mozilla/5.0" +} +} +}, +"payload":{ +"amount":"12.500", +"billing_address":{ +"city":"ptok:v1:fKrE4cXxHekZk4mklS_6gF9X89UW19yWC8H9AaftrcE", +"country":"ptok:v1:9hw5Sp-i6dZADYPdeIHwEOV3awFcLyE-i2AgY5JGTGs", +"line1":"ptok:v1:Ts0ReoBNKiJBYHXTW1WbXX7iUnDW6VwUwCUjR_LLNNY" +}, +"card_acceptance_criteria":{ +"min_expiry_time":30 +}, +"currency_code":"KWD", +"customer_email":"ptok:v1:eMwFkFHAafEM2yUlbFIkhBKSUhHqDcNpxE3bLyWFI54", +"customer_first_name":"ptok:v1:NVSAuFYU9wWRKK_cQt9wbZTw0HOXuh5NgHiMxgv64o8", +"customer_phone":"ptok:v1:kVcmxuM7xSNaFxT-GvmeIwsb_FQMPsR72OBNUiZEAQc", +"extra":{ +"items":[ +{ +"qty":0, +"sku":"SKU-0" +}, +{ +"qty":1, +"sku":"SKU-1" +}, +{ +"qty":2, +"sku":"SKU-2" +}, +{ +"qty":3, +"sku":"SKU-3" +}, +{ +"qty":4, +"sku":"SKU-4" +}, +{ +"qty":5, +"sku":"SKU-5" +}, +{ +"qty":6, +"sku":"SKU-6" +}, +{ +"qty":7, +"sku":"SKU-7" +}, +{ +"qty":8, +"sku":"SKU-8" +}, +{ +"qty":9, +"sku":"SKU-9" +} +], +"note":"gift wrap" +}, +"order_no":"ORD-2026-000123", +"pg_codes":[ +"mpgs-direct", +"cs-direct", +"kpay" +], +"redirect_url":"https://merchant.example/return?ref=abc123&lang=en", +"session_id":"21a2496f8b6c193ec61b8dd52cbe8d827b4eaa49", +"three_ds":{ +"directory_server_transaction_id":"3d85c5d3-8412-4f1d-9e26-4a0f00123456" +}, +"webhook_url":"https://merchant.example/hooks/ottu" +}, +"url":{ +"full":"https://jade.ottu.dev/b/checkout/v1/pymt-txn/?utm=1" +} +}, +{ +"event":"pg.response_received from apitest.cybersource.com", +"http":{ +"request":{ +"method":"POST" +}, +"response":{ +"body":"{\"id\":\"pay_9f8e7d6c5b4a\",\"status\":\"AUTHORIZED\",\"amount\":\"100.000\",\"currency\":\"KWD\",\"processorInformation\":{\"approvalCode\":\"831000\",\"responseCode\":\"100\",\"transactionId\":\"7266500001234567\"},\"orderInformation\":{\"amountDetails\":{\"authorizedAmount\":\"100.000\",\"currency\":\"KWD\"}},\"_links\":{\"self\":{\"href\":\"/pts/v2/payments/7266500001234567\"}}}", +"status_code":201 +} +}, +"labels":{ +"direction":"outbound", +"namespace":"cybersource", +"operation":"payment", +"pg_result":"AUTHORIZED" +}, +"merchant_id":"jade.ottu.dev", +"payment":{ +"pg_code":"cs-direct", +"reference":"jade-oTTE45" +}, +"service":{ +"target":{ +"name":"cybersource" +} +}, +"session_id":"b1e2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0", +"url":{ +"full":"https://apitest.cybersource.com/pts/v2/payments/" +} +}, +{ +"event":"pg.response_received from apitest.cybersource.com", +"http":{ +"request":{ +"method":"POST" +}, +"response":{ +"body":"{\"access_token\": \"ptok:v1:iWjJ0BTFxqptSpf9yTbBc2WW0qn3L3x0WxlYMRSohJI\", \"id\": \"pay_9f8e7d6c5b4a\", \"status\": \"AUTHORIZED\", \"amount\": \"100.000\", \"currency\": \"KWD\", \"processorInformation\": {\"approvalCode\": \"831000\", \"responseCode\": \"100\", \"transactionId\": \"7266500001234567\"}, \"orderInformation\": {\"amountDetails\": {\"authorizedAmount\": \"100.000\", \"currency\": \"KWD\"}}, \"_links\": {\"self\": {\"href\": \"/pts/v2/payments/7266500001234567\"}}}", +"status_code":201 +} +}, +"labels":{ +"direction":"outbound", +"namespace":"cybersource", +"operation":"payment", +"pg_result":"AUTHORIZED" +}, +"merchant_id":"jade.ottu.dev", +"payment":{ +"pg_code":"cs-direct", +"reference":"jade-oTTE45" +}, +"service":{ +"target":{ +"name":"cybersource" +} +}, +"session_id":"b1e2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0", +"url":{ +"full":"https://apitest.cybersource.com/pts/v2/payments/" +} +}, +{ +"event":"pg.request_sent to MPGS", +"message_text":"card 4111 1111 1111 1111 cvv [CVV-MASKED] for ptok:v1:ncKKpBImcKIh91as3qZfSn9AhHZTyyFjBOnZfwq4xkY", +"payload":{ +"customer":{ +"email":"ptok:v1:ncKKpBImcKIh91as3qZfSn9AhHZTyyFjBOnZfwq4xkY", +"firstName":"ptok:v1:NVSAuFYU9wWRKK_cQt9wbZTw0HOXuh5NgHiMxgv64o8" +}, +"order":{ +"amount":20, +"reference":"deltabRKJ5X_0" +}, +"sourceOfFunds":{ +"provided":{ +"card":{ +"expiry":{ +"month":"01", +"year":"27" +}, +"number":"411111******1111", +"securityCode":"[CVV-MASKED]" +} +} +} +}, +"session_id":"b1e2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0" +}, +{ +"event":"create payment", +"token":"ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"cvv":"[CVV-MASKED]" +}, +{ +"cvv":"[CVV-MASKED]" +}, +{ +"session_key":"ptok:v1:8O9tqpAd1nMMT3yelPozCFaxMX7IiICgfgF3iSXjz8k" +}, +{ +"access_token":null +}, +{ +"card":null, +"customer_name":null, +"cvv":null, +"expiry":null, +"public_key":null +}, +{ +"Cvv":"[CVV-MASKED]" +}, +{ +"securityCode":"[CVV-MASKED]" +}, +{ +"data":{ +"token":{ +"more":"ptok:v1:7ZWOgZZFY3UYwHRVga6PD6VriwK0ettMsFZOMt0d2IY", +"nested":"ptok:v1:8ThbeH-dNcoVUO63vcVSzMeivjAFF4neP158xd6Czm0" +} +} +}, +{ +"payload":{ +"note":"[SAD-MASKED]", +"paymentData":"[SAD-MASKED]" +} +}, +{ +"http":{ +"request":{ +"body":{ +"content":"TRANPORTAL123ptok:v1:AQ6W6lJiUPhhfswZOx3SLOQ5DZ_fAoBspUjCrPV_4lA110.000TRK1{\"paymentData\":\"[SAD-MASKED]\"}" +} +} +} +}, +{ +"http":{ +"request":{ +"body":{ +"content":"TRANPORTAL123ptok:v1:AQ6W6lJiUPhhfswZOx3SLOQ5DZ_fAoBspUjCrPV_4lA810.000202612345TRK1TrackID" +} +} +} +}, +{ +"http":{ +"request":{ +"body":{ +"content":"TRANPORTAL123ptok:v1:AQ6W6lJiUPhhfswZOx3SLOQ5DZ_fAoBspUjCrPV_4lA25.000202612345TRK1TrackID" +} +} +} +}, +{ +"event":"ptok:v1:BCng-t9pGDNJZb8Kxr-8dbnx0vH9S1VGixXZwTjuAXw" +}, +{ +"event":"ptok:v1:fbBTZVC9H_ZhvwccVlIcAT81aJ11pahAQwMADLNidjk" +}, +{ +"event":"ptok:v1:KQvnEFp44z39aJFKt8AgvxsnNi558q_g7PpLLFmjhU8" +}, +{ +"event":"ptok:v1:IPPblK6aMP_OH1mPa_NmJEku5a8A4DNuj0e8_FswPm0" +}, +{ +"event":"ptok:v1:nMYkVx-T7gC81k-ugXTnuEVARP23rbgqh68K2StP3Fw" +}, +{ +"event":"'token': 'ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo'" +}, +{ +"event":"'token':'ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo'" +}, +{ +"event":"\"token\": \"ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo\"" +}, +{ +"event":"\"token\":\"ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo\"" +}, +{ +"event":"token: ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"token:ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"token= ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"token=ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"token=ptok:v1:7ZWOgZZFY3UYwHRVga6PD6VriwK0ettMsFZOMt0d2IY" +}, +{ +"event":"token=ptok:v1:JuuCkMnRlUJr5bdNk7w-vsvpb9mqq7UBGofK_xBj-0I" +}, +{ +"event":"token: ptok:v1:7ZWOgZZFY3UYwHRVga6PD6VriwK0ettMsFZOMt0d2IY" +}, +{ +"event":"token: ptok:v1:JuuCkMnRlUJr5bdNk7w-vsvpb9mqq7UBGofK_xBj-0I" +}, +{ +"event":"token:ptok:v1:_lXByvUrQgAiZ4EOeOXGT_PlbUd0M_GNzfg43nNBFX4" +}, +{ +"event":"token=ptok:v1:_lXByvUrQgAiZ4EOeOXGT_PlbUd0M_GNzfg43nNBFX4" +}, +{ +"event":"token:ptok:v1:OGFRjEzuVhZrn1tlwWeQ5zNvk4_khFjSFKrMQY_YIME" +}, +{ +"event":"token=ptok:v1:OGFRjEzuVhZrn1tlwWeQ5zNvk4_khFjSFKrMQY_YIME" +}, +{ +"event":"token ptok:v1:_lXByvUrQgAiZ4EOeOXGT_PlbUd0M_GNzfg43nNBFX4" +}, +{ +"event":"token ptok:v1:OGFRjEzuVhZrn1tlwWeQ5zNvk4_khFjSFKrMQY_YIME" +}, +{ +"event":"token ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"message token=ptok:v1:7ZWOgZZFY3UYwHRVga6PD6VriwK0ettMsFZOMt0d2IY" +}, +{ +"event":"anyword_token ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"any_word_token ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"any-word_token ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"any-word-token ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"any_1-word_2-token ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"secret ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"any_1-word_2-secret ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"password ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"any_1-word_2-password ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"passwd ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"any_1-word_2-passwd ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"bearer ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"basic ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"digest ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"credential ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"credentials ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"authorization ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"authorization_header ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"authorization-header ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"authorisation ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"authorisation_header ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"authorisation-header ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"apikey ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"api_key ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"ptok:v1:BPI3sZpAYaaGwAQC5vdX9NK56OcYPacA_IRmfsKNz18" +}, +{ +"event":"secret-key ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"private-key ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"public-key ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"encryption-key ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"decryption-key ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"signing-key ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"access-key ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"master-key ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"root-key ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"session-key ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"secret_key=[SECRET-MASKED]" +}, +{ +"event":"'Authorization': 'ptok:v1:hG5NYyP8muNdI04cUjGKgpRa2ci25G--CJhvC722PJI'" +}, +{ +"event":"\"Authorization\": \"ptok:v1:hG5NYyP8muNdI04cUjGKgpRa2ci25G--CJhvC722PJI\"" +}, +{ +"event":"Authorization: ptok:v1:hG5NYyP8muNdI04cUjGKgpRa2ci25G--CJhvC722PJI" +}, +{ +"event":"Authorization= ptok:v1:hG5NYyP8muNdI04cUjGKgpRa2ci25G--CJhvC722PJI" +}, +{ +"event":"Authorization Bearer ptok:v1:OGFRjEzuVhZrn1tlwWeQ5zNvk4_khFjSFKrMQY_YIME" +}, +{ +"event":"{\"Authorization\": \"ptok:v1:_lXByvUrQgAiZ4EOeOXGT_PlbUd0M_GNzfg43nNBFX4\"}" +}, +{ +"event":"Here is {\"Authorization\": \"ptok:v1:_lXByvUrQgAiZ4EOeOXGT_PlbUd0M_GNzfg43nNBFX4\"}" +}, +{ +"event":"{\"Authorization\": \"ptok:v1:xwZelCJLrv6In2A4aVrz2O6_bPGyND5B0MDxmR0hmkM\"}" +}, +{ +"event":"Authorization: ptok:v1:Dp949MnXQasK4lMRYeS1Qn45Pljm-vbis-75-lgeiLw" +}, +{ +"event":"{\"Authorisation\": \"ptok:v1:Dp949MnXQasK4lMRYeS1Qn45Pljm-vbis-75-lgeiLw\"}" +}, +{ +"event":"Authentication failed with key 'ptok:v1:qDIRNe74Q6tSmwUe4fuytRREKT2HVfKEkFL8Z6h4egA'" +}, +{ +"event":"access_token=ptok:v1:TSsQJnxs4gq-u3wovFm24skGr5SOOxZJDuZcby0wbNk" +}, +{ +"event":"refresh_token=ptok:v1:TSsQJnxs4gq-u3wovFm24skGr5SOOxZJDuZcby0wbNk" +}, +{ +"event":"client_secret=ptok:v1:1Uc-bob3ywEPV_TX7RuoxshPDdrvBnjpYJK7cp-hYYY" +}, +{ +"event":"private_key=ptok:v1:TSsQJnxs4gq-u3wovFm24skGr5SOOxZJDuZcby0wbNk" +}, +{ +"event":"secret_key=ptok:v1:TSsQJnxs4gq-u3wovFm24skGr5SOOxZJDuZcby0wbNk" +}, +{ +"event":"basic= ptok:v1:_lXByvUrQgAiZ4EOeOXGT_PlbUd0M_GNzfg43nNBFX4" +}, +{ +"event":"ptok:v1:qDIRNe74Q6tSmwUe4fuytRREKT2HVfKEkFL8Z6h4egA" +}, +{ +"event":"Basic ptok:v1:dDYYfBksvPcnCNHNFCAelE8oUzi93GeVTmKUAF8nvqk" +}, +{ +"event":"body {\"public_key\": null, \"a\": 1}" +}, +{ +"event":"body {\"session_key\": true}" +}, +{ +"event":"body {'public_key': None}" +}, +{ +"event":"body {\"session_key\": \"ptok:v1:8O9tqpAd1nMMT3yelPozCFaxMX7IiICgfgF3iSXjz8k\"}" +}, +{ +"event":"body {'api_key': 'ptok:v1:8O9tqpAd1nMMT3yelPozCFaxMX7IiICgfgF3iSXjz8k'}" +}, +{ +"event":"{\"result\": \"SUCCESS\", \"session\": {\"aes256Key\": \"ptok:v1:kK2jZTAmp0_y-_RZE2TP3cz8F67sFwElkvPbQ4Hz4wI\", \"id\": \"SESS" +}, +{ +"event":"aes256Key=ptok:v1:kK2jZTAmp0_y-_RZE2TP3cz8F67sFwElkvPbQ4Hz4wI" +}, +{ +"event":"hmacSha256Key=ptok:v1:TSsQJnxs4gq-u3wovFm24skGr5SOOxZJDuZcby0wbNk" +}, +{ +"event":"privateKeyPem=ptok:v1:TSsQJnxs4gq-u3wovFm24skGr5SOOxZJDuZcby0wbNk" +}, +{ +"event":"codes_key=abcd1234efgh5678" +}, +{ +"event":"'cvv': '[CVV-MASKED]'" +}, +{ +"event":"'Cvv':'[CVV-MASKED]'" +}, +{ +"event":"\"cVv\": \"[CVV-MASKED]\"" +}, +{ +"event":"\"cvV\":\"[CVV-MASKED]\"" +}, +{ +"event":"CVv: [CVV-MASKED]" +}, +{ +"event":"cVV:[CVV-MASKED]" +}, +{ +"event":"CvV= [CVV-MASKED]" +}, +{ +"event":"CVV=[CVV-MASKED]" +}, +{ +"event":"Here cvv=[CVV-MASKED] is submitted" +}, +{ +"event":"'cvc': '[CVV-MASKED]'" +}, +{ +"event":"'CVC': '[CVV-MASKED]'" +}, +{ +"event":"{\"processed_data\": {\"cvv\": \"[CVV-MASKED]\"}}" +}, +{ +"event":"data {\"cvv\": \"[CVV-MASKED]\"}" +}, +{ +"event":"payment_id: abc12345" +}, +{ +"event":"payment_id= abc12345" +}, +{ +"event":"payment_id abc12345" +}, +{ +"event":"payment-id: abc12345" +}, +{ +"event":"payment-id= abc12345" +}, +{ +"event":"payment-id abc12345" +}, +{ +"event":"'payment_id': 'abc12345'" +}, +{ +"event":"'payment-id': 'abc12345'" +}, +{ +"event":"'payment_id':'abc12345'" +}, +{ +"event":"'payment-id':'abc12345'" +}, +{ +"event":"\"payment_id\": \"abc12345\"" +}, +{ +"event":"\"payment-id\": \"abc12345\"" +}, +{ +"event":"\"payment_id\":\"abc12345\"" +}, +{ +"event":"\"payment-id\":\"abc12345\"" +}, +{ +"event":"'transaction_id': 'jbzzTT577'" +}, +{ +"event":"'auth_id': '016153570198200'" +}, +{ +"event":"payment_id: 1234567890123456 done" +}, +{ +"event":"payment_id= 9876543210987654 done" +}, +{ +"event":"transaction_id: abcd1234" +}, +{ +"event":"transaction-id: abcd1234" +}, +{ +"event":"auth_id: abcd1234" +}, +{ +"event":"auth-id: abcd1234" +}, +{ +"event":"{\"processed_data\": {\"payment_id\": \"abc12345\"}}" +}, +{ +"event":"account GB33BUKB20201555555555 credited" +}, +{ +"event":"acct BE68539007547034 debited" +}, +{ +"event":"acct FR1420041010050500013M02606 debited" +}, +{ +"event":"acct BH67BMAG00001299123456 debited" +}, +{ +"event":"acct QA58DOHB00001234567890ABCDEFG debited" +}, +{ +"event":"acct GB1212345678901 debited" +}, +{ +"event":"acct GB3412345678901234 debited" +}, +{ +"event":"acct GB5612345678901234567 debited" +}, +{ +"event":"acct GB78123456789012345678 debited" +}, +{ +"event":"ptok:v1:gDlYdUIUkQZbmus1FJLlEYAUhdtLW3TlTUklf7eqj2c" +}, +{ +"event":"ptok:v1:gDlYdUIUkQZbmus1FJLlEYAUhdtLW3TlTUklf7eqj2c" +}, +{ +"event":"ptok:v1:gDlYdUIUkQZbmus1FJLlEYAUhdtLW3TlTUklf7eqj2c" +}, +{ +"event":"ptok:v1:gDlYdUIUkQZbmus1FJLlEYAUhdtLW3TlTUklf7eqj2c" +}, +{ +"event":"ptok:v1:lOiafQukK2ddrC6Gd_QK0g8aVvusxxigzv7LyZ1nRhA" +}, +{ +"event":"ptok:v1:lOiafQukK2ddrC6Gd_QK0g8aVvusxxigzv7LyZ1nRhA" +}, +{ +"event":"ptok:v1:lOiafQukK2ddrC6Gd_QK0g8aVvusxxigzv7LyZ1nRhA" +}, +{ +"event":"call ptok:v1:emM4ILoMX6tGxIQiyDns7y52AJRkhUmFm-Mu6BQp0bI now" +}, +{ +"event":"call ptok:v1:BFQxttUVRdBVP4IXaQcjudawbolAO_1Jsith6usx2ik now" +}, +{ +"event":"ptok:v1:rYi1lZa-Lxx9KQISmPti_1Xzvhivf27170HDufgjs6k" +}, +{ +"event":"contact ptok:v1:AoW38Cwh4OVIZRwWz5Lx2r81wisRN6haQhTnKkioXfU now" +}, +{ +"event":"ptok:v1:4xaL1xJcFwXzMA7EZEloqnMYKkePjN6CC9k4p9jmLHc" +}, +{ +"event":"ptok:v1:g2KSE-5SadRSm3fNuE_ZSwIkQYnNuMdFGiTgE7-KlEc" +}, +{ +"event":"ptok:v1:PeHcgjnNgkX5SwPisbS-3ugkutSN4sDeIk0j4PX5Qpk" +}, +{ +"event":"ptok:v1:s-XuwaGJfe2GEohzYhQufQ2gI57y-YCTg_ihqcSXA5k" +}, +{ +"event":"ptok:v1:0V6B6wiwdYZp7_wQtGNkFYmgqQE2ZLwZBTo19Grie14" +}, +{ +"event":"ptok:v1:KFfEaU4te4Kb3ZJ6QUZSYBDtvE_axEbGZnzseQbm778" +}, +{ +"event":"ptok:v1:LBFNre8vo3auF2ADj_XKMWVkUE0ls0ivCmdkSOmWmjQ" +}, +{ +"event":"notify: ptok:v1:EXeEqO-xvgQ0M4yCP07Hdqsjc-5V_Z5PJcSqnwaKTf8" +}, +{ +"event":"'email': 'ptok:v1:rYi1lZa-Lxx9KQISmPti_1Xzvhivf27170HDufgjs6k'" +}, +{ +"event":"\"email\": \"ptok:v1:rYi1lZa-Lxx9KQISmPti_1Xzvhivf27170HDufgjs6k\"" +}, +{ +"event":"email=ptok:v1:rYi1lZa-Lxx9KQISmPti_1Xzvhivf27170HDufgjs6k" +}, +{ +"event":"{\"contact\": {\"email\": \"ptok:v1:rYi1lZa-Lxx9KQISmPti_1Xzvhivf27170HDufgjs6k\"}}" +}, +{ +"event":"ptok:v1:_aWRbsN5VB9K61ABl7eN52wywzA1JU0kHB1w9CYLag8" +}, +{ +"event":"Here it's ptok:v1:_aWRbsN5VB9K61ABl7eN52wywzA1JU0kHB1w9CYLag8 failed!" +}, +{ +"event":"Here it's (ptok:v1:_aWRbsN5VB9K61ABl7eN52wywzA1JU0kHB1w9CYLag8) failed!" +}, +{ +"event":"Here it's: ptok:v1:_aWRbsN5VB9K61ABl7eN52wywzA1JU0kHB1w9CYLag8 failed!" +}, +{ +"event":"-ptok:v1:_aWRbsN5VB9K61ABl7eN52wywzA1JU0kHB1w9CYLag8" +}, +{ +"event":".ptok:v1:_aWRbsN5VB9K61ABl7eN52wywzA1JU0kHB1w9CYLag8" +}, +{ +"event":"ptok:v1:kRIP0Oc8sUQzmwjZ2ju_0DG7sPYsSbMplGr3IpkgdEA" +}, +{ +"event":"ptok:v1:QGj0Qfq3ti-UT28uqfOsofZyqMb3HscxUFds5M6-1hM" +}, +{ +"event":"ptok:v1:Z8KistoEjGGA6sr8Z9AldbgUYQQC_bEfUHTspNUByhU" +}, +{ +"event":"ptok:v1:at70G4JsvJ30mAKBl53VcNTUFi8FYK0W6puL5-mefNc" +}, +{ +"event":"token was ptok:v1:CBoJ8vE3I9u5CvS1CbI4JvXxtn4kSPZrLl7ToDtKjFo and more" +}, +{ +"event":"912345678912" +}, +{ +"event":"9123456789123" +}, +{ +"event":"91234567891234" +}, +{ +"event":"912345678912345" +}, +{ +"event":"9123456789123456" +}, +{ +"event":"91234567891234567" +}, +{ +"event":"912345678912345678" +}, +{ +"event":"9123456789123456789" +}, +{ +"event":"9123 4567 8912" +}, +{ +"event":"9123 4567 8912 3" +}, +{ +"event":"9123 4567 8912 34" +}, +{ +"event":"9123 4567 8912 345" +}, +{ +"event":"9123 4567 8912 3456" +}, +{ +"event":"9123 4567 8912 34567" +}, +{ +"event":"9123 4567 8912 345678" +}, +{ +"event":"9123 4567 8912 3456789" +}, +{ +"event":"9123-4567-8912" +}, +{ +"event":"9123-4567-8912-3" +}, +{ +"event":"9123-4567-8912-34" +}, +{ +"event":"9123-4567-8912-345" +}, +{ +"event":"9123-4567-8912-3456" +}, +{ +"event":"9123-4567-8912-34567" +}, +{ +"event":"9123-4567-8912-345678" +}, +{ +"event":"9123-4567-8912-3456789" +}, +{ +"event":"112345678912" +}, +{ +"event":"1123456789123" +}, +{ +"event":"11234567891234" +}, +{ +"event":"112345678912345" +}, +{ +"event":"1123456789123456" +}, +{ +"event":"11234567891234567" +}, +{ +"event":"112345678912345678" +} +], +"keyset/default/prose":[ +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +"{\"session_id\": \"21a2496f8b6c193ec61b8dd52cbe8d827b4eaa49\", \"order_no\": \"ORD-2026-000123\", \"amount\": \"12.500\", \"currency_code\": \"KWD\", \"customer_email\": \"ptok:v1:eMwFkFHAafEM2yUlbFIkhBKSUhHqDcNpxE3bLyWFI54\", \"customer_phone\": \"ptok:v1:kVcmxuM7xSNaFxT-GvmeIwsb_FQMPsR72OBNUiZEAQc\", \"customer_first_name\": \"ptok:v1:NVSAuFYU9wWRKK_cQt9wbZTw0HOXuh5NgHiMxgv64o8\", \"billing_address\": {\"line1\": \"ptok:v1:Ts0ReoBNKiJBYHXTW1WbXX7iUnDW6VwUwCUjR_LLNNY\", \"city\": \"ptok:v1:fKrE4cXxHekZk4mklS_6gF9X89UW19yWC8H9AaftrcE\", \"country\": \"ptok:v1:9hw5Sp-i6dZADYPdeIHwEOV3awFcLyE-i2AgY5JGTGs\"}, \"pg_codes\": [\"mpgs-direct\", \"cs-direct\", \"kpay\"], \"redirect_url\": \"https://merchant.example/return?ref=abc123&lang=en\", \"webhook_url\": \"https://merchant.example/hooks/ottu\", \"extra\": {\"note\": \"gift wrap\", \"items\": [{\"sku\": \"SKU-0\", \"qty\": 0}, {\"sku\": \"SKU-1\", \"qty\": 1}, {\"sku\": \"SKU-2\", \"qty\": 2}, {\"sku\": \"SKU-3\", \"qty\": 3}, {\"sku\": \"SKU-4\", \"qty\": 4}, {\"sku\": \"SKU-5\", \"qty\": 5}, {\"sku\": \"SKU-6\", \"qty\": 6}, {\"sku\": \"SKU-7\", \"qty\": 7}, {\"sku\": \"SKU-8\", \"qty\": 8}, {\"sku\": \"SKU-9\", \"qty\": 9}]}, \"card_acceptance_criteria\": {\"min_expiry_time\": 30}, \"three_ds\": {\"directory_server_transaction_id\": \"3d85c5d3-8412-4f1d-9e26-4a0f00123456\"}}", +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null +], +"keyset/default/records":[ +[ +"user %s paid %s KWD for %s", +"('bob', '12.500', 'ORD-1')", +"user bob paid 12.500 KWD for ORD-1" +], +[ +"cvv=[CVV-MASKED]", +"()", +"cvv=[CVV-MASKED]" +], +[ +"card %s %s", +"('4111111111111111', '123')", +"card 4111111111111111 123" +], +[ +"password=ptok:v1:YAlHtTNhC_kDTbpjnErrOlkwieoPRffgVWW1cGq95zk", +"()", +"password=ptok:v1:YAlHtTNhC_kDTbpjnErrOlkwieoPRffgVWW1cGq95zk" +], +[ +"paid 10 KWD by ptok:v1:8NokGXtoHzu2rwxmzOBYr1mRB7NV7_DRf2UFGjENzpM", +"()", +"paid 10 KWD by ptok:v1:8NokGXtoHzu2rwxmzOBYr1mRB7NV7_DRf2UFGjENzpM" +], +[ +"sent to %s", +"('ptok:v1:q-aBDA-3PLgNB0wyeH6tCsUcbaLAm6sTRaHYrmp7nGI',)", +"sent to ptok:v1:q-aBDA-3PLgNB0wyeH6tCsUcbaLAm6sTRaHYrmp7nGI" +], +[ +"apple pay token %s", +"('[SAD-MASKED]',)", +"apple pay token [SAD-MASKED]" +], +[ +"retried with Bearer ptok:v1:iWjJ0BTFxqptSpf9yTbBc2WW0qn3L3x0WxlYMRSohJI", +"()", +"retried with Bearer ptok:v1:iWjJ0BTFxqptSpf9yTbBc2WW0qn3L3x0WxlYMRSohJI" +], +[ +"%s", +"{'card_number': '411111******1111', 'cvv': '[CVV-MASKED]'}", +"{'card_number': '411111******1111', 'cvv': '[CVV-MASKED]'}" +] +], +"keyset/default/text":[ +"ptok:v1:BCng-t9pGDNJZb8Kxr-8dbnx0vH9S1VGixXZwTjuAXw", +"ptok:v1:fbBTZVC9H_ZhvwccVlIcAT81aJ11pahAQwMADLNidjk", +"ptok:v1:KQvnEFp44z39aJFKt8AgvxsnNi558q_g7PpLLFmjhU8", +"ptok:v1:IPPblK6aMP_OH1mPa_NmJEku5a8A4DNuj0e8_FswPm0", +"ptok:v1:nMYkVx-T7gC81k-ugXTnuEVARP23rbgqh68K2StP3Fw", +"'token': 'ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo'", +"'token':'ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo'", +"\"token\": \"ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo\"", +"\"token\":\"ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo\"", +"token: ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"token:ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"token= ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"token=ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"token=ptok:v1:7ZWOgZZFY3UYwHRVga6PD6VriwK0ettMsFZOMt0d2IY", +"token=ptok:v1:JuuCkMnRlUJr5bdNk7w-vsvpb9mqq7UBGofK_xBj-0I", +"token: ptok:v1:7ZWOgZZFY3UYwHRVga6PD6VriwK0ettMsFZOMt0d2IY", +"token: ptok:v1:JuuCkMnRlUJr5bdNk7w-vsvpb9mqq7UBGofK_xBj-0I", +"token:ptok:v1:_lXByvUrQgAiZ4EOeOXGT_PlbUd0M_GNzfg43nNBFX4", +"token=ptok:v1:_lXByvUrQgAiZ4EOeOXGT_PlbUd0M_GNzfg43nNBFX4", +"token:ptok:v1:OGFRjEzuVhZrn1tlwWeQ5zNvk4_khFjSFKrMQY_YIME", +"token=ptok:v1:OGFRjEzuVhZrn1tlwWeQ5zNvk4_khFjSFKrMQY_YIME", +"token ptok:v1:_lXByvUrQgAiZ4EOeOXGT_PlbUd0M_GNzfg43nNBFX4", +"token ptok:v1:OGFRjEzuVhZrn1tlwWeQ5zNvk4_khFjSFKrMQY_YIME", +"token ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"message token=ptok:v1:7ZWOgZZFY3UYwHRVga6PD6VriwK0ettMsFZOMt0d2IY", +"anyword_token ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"any_word_token ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"any-word_token ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"any-word-token ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"any_1-word_2-token ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"secret ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"any_1-word_2-secret ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"password ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"any_1-word_2-password ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"passwd ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"any_1-word_2-passwd ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"bearer ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"basic ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"digest ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"credential ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"credentials ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"authorization ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"authorization_header ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"authorization-header ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"authorisation ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"authorisation_header ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"authorisation-header ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"apikey ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"api_key ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"ptok:v1:BPI3sZpAYaaGwAQC5vdX9NK56OcYPacA_IRmfsKNz18", +"secret-key ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"private-key ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"public-key ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"encryption-key ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"decryption-key ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"signing-key ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"access-key ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"master-key ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"root-key ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"session-key ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"secret_key=[SECRET-MASKED]", +"'Authorization': 'ptok:v1:hG5NYyP8muNdI04cUjGKgpRa2ci25G--CJhvC722PJI'", +"\"Authorization\": \"ptok:v1:hG5NYyP8muNdI04cUjGKgpRa2ci25G--CJhvC722PJI\"", +"Authorization: ptok:v1:hG5NYyP8muNdI04cUjGKgpRa2ci25G--CJhvC722PJI", +"Authorization= ptok:v1:hG5NYyP8muNdI04cUjGKgpRa2ci25G--CJhvC722PJI", +"Authorization Bearer ptok:v1:OGFRjEzuVhZrn1tlwWeQ5zNvk4_khFjSFKrMQY_YIME", +"{\"Authorization\": \"ptok:v1:_lXByvUrQgAiZ4EOeOXGT_PlbUd0M_GNzfg43nNBFX4\"}", +"Here is {\"Authorization\": \"ptok:v1:_lXByvUrQgAiZ4EOeOXGT_PlbUd0M_GNzfg43nNBFX4\"}", +"{\"Authorization\": \"ptok:v1:xwZelCJLrv6In2A4aVrz2O6_bPGyND5B0MDxmR0hmkM\"}", +"Authorization: ptok:v1:Dp949MnXQasK4lMRYeS1Qn45Pljm-vbis-75-lgeiLw", +"{\"Authorisation\": \"ptok:v1:Dp949MnXQasK4lMRYeS1Qn45Pljm-vbis-75-lgeiLw\"}", +"Authentication failed with key 'ptok:v1:qDIRNe74Q6tSmwUe4fuytRREKT2HVfKEkFL8Z6h4egA'", +"access_token=ptok:v1:TSsQJnxs4gq-u3wovFm24skGr5SOOxZJDuZcby0wbNk", +"refresh_token=ptok:v1:TSsQJnxs4gq-u3wovFm24skGr5SOOxZJDuZcby0wbNk", +"client_secret=ptok:v1:1Uc-bob3ywEPV_TX7RuoxshPDdrvBnjpYJK7cp-hYYY", +"private_key=ptok:v1:TSsQJnxs4gq-u3wovFm24skGr5SOOxZJDuZcby0wbNk", +"secret_key=ptok:v1:TSsQJnxs4gq-u3wovFm24skGr5SOOxZJDuZcby0wbNk", +"basic= ptok:v1:_lXByvUrQgAiZ4EOeOXGT_PlbUd0M_GNzfg43nNBFX4", +"ptok:v1:qDIRNe74Q6tSmwUe4fuytRREKT2HVfKEkFL8Z6h4egA", +"Basic ptok:v1:dDYYfBksvPcnCNHNFCAelE8oUzi93GeVTmKUAF8nvqk", +null, +null, +null, +"body {\"session_key\": \"ptok:v1:8O9tqpAd1nMMT3yelPozCFaxMX7IiICgfgF3iSXjz8k\"}", +"body {'api_key': 'ptok:v1:8O9tqpAd1nMMT3yelPozCFaxMX7IiICgfgF3iSXjz8k'}", +"{\"result\": \"SUCCESS\", \"session\": {\"aes256Key\": \"ptok:v1:kK2jZTAmp0_y-_RZE2TP3cz8F67sFwElkvPbQ4Hz4wI\", \"id\": \"SESS", +"aes256Key=ptok:v1:kK2jZTAmp0_y-_RZE2TP3cz8F67sFwElkvPbQ4Hz4wI", +"hmacSha256Key=ptok:v1:TSsQJnxs4gq-u3wovFm24skGr5SOOxZJDuZcby0wbNk", +"privateKeyPem=ptok:v1:TSsQJnxs4gq-u3wovFm24skGr5SOOxZJDuZcby0wbNk", +null, +"'cvv': '[CVV-MASKED]'", +"'Cvv':'[CVV-MASKED]'", +"\"cVv\": \"[CVV-MASKED]\"", +"\"cvV\":\"[CVV-MASKED]\"", +"CVv: [CVV-MASKED]", +"cVV:[CVV-MASKED]", +"CvV= [CVV-MASKED]", +"CVV=[CVV-MASKED]", +"Here cvv=[CVV-MASKED] is submitted", +"'cvc': '[CVV-MASKED]'", +"'CVC': '[CVV-MASKED]'", +"{\"processed_data\": {\"cvv\": \"[CVV-MASKED]\"}}", +"data {\"cvv\": \"[CVV-MASKED]\"}", +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +"ptok:v1:gDlYdUIUkQZbmus1FJLlEYAUhdtLW3TlTUklf7eqj2c", +"ptok:v1:gDlYdUIUkQZbmus1FJLlEYAUhdtLW3TlTUklf7eqj2c", +"ptok:v1:gDlYdUIUkQZbmus1FJLlEYAUhdtLW3TlTUklf7eqj2c", +"ptok:v1:gDlYdUIUkQZbmus1FJLlEYAUhdtLW3TlTUklf7eqj2c", +"ptok:v1:lOiafQukK2ddrC6Gd_QK0g8aVvusxxigzv7LyZ1nRhA", +"ptok:v1:lOiafQukK2ddrC6Gd_QK0g8aVvusxxigzv7LyZ1nRhA", +"ptok:v1:lOiafQukK2ddrC6Gd_QK0g8aVvusxxigzv7LyZ1nRhA", +"call ptok:v1:emM4ILoMX6tGxIQiyDns7y52AJRkhUmFm-Mu6BQp0bI now", +"call ptok:v1:BFQxttUVRdBVP4IXaQcjudawbolAO_1Jsith6usx2ik now", +"ptok:v1:rYi1lZa-Lxx9KQISmPti_1Xzvhivf27170HDufgjs6k", +"contact ptok:v1:AoW38Cwh4OVIZRwWz5Lx2r81wisRN6haQhTnKkioXfU now", +"ptok:v1:4xaL1xJcFwXzMA7EZEloqnMYKkePjN6CC9k4p9jmLHc", +"ptok:v1:g2KSE-5SadRSm3fNuE_ZSwIkQYnNuMdFGiTgE7-KlEc", +"ptok:v1:PeHcgjnNgkX5SwPisbS-3ugkutSN4sDeIk0j4PX5Qpk", +"ptok:v1:s-XuwaGJfe2GEohzYhQufQ2gI57y-YCTg_ihqcSXA5k", +"ptok:v1:0V6B6wiwdYZp7_wQtGNkFYmgqQE2ZLwZBTo19Grie14", +"ptok:v1:KFfEaU4te4Kb3ZJ6QUZSYBDtvE_axEbGZnzseQbm778", +"ptok:v1:LBFNre8vo3auF2ADj_XKMWVkUE0ls0ivCmdkSOmWmjQ", +"notify: ptok:v1:EXeEqO-xvgQ0M4yCP07Hdqsjc-5V_Z5PJcSqnwaKTf8", +"'email': 'ptok:v1:rYi1lZa-Lxx9KQISmPti_1Xzvhivf27170HDufgjs6k'", +"\"email\": \"ptok:v1:rYi1lZa-Lxx9KQISmPti_1Xzvhivf27170HDufgjs6k\"", +"email=ptok:v1:rYi1lZa-Lxx9KQISmPti_1Xzvhivf27170HDufgjs6k", +"{\"contact\": {\"email\": \"ptok:v1:rYi1lZa-Lxx9KQISmPti_1Xzvhivf27170HDufgjs6k\"}}", +"ptok:v1:_aWRbsN5VB9K61ABl7eN52wywzA1JU0kHB1w9CYLag8", +"Here it's ptok:v1:_aWRbsN5VB9K61ABl7eN52wywzA1JU0kHB1w9CYLag8 failed!", +"Here it's (ptok:v1:_aWRbsN5VB9K61ABl7eN52wywzA1JU0kHB1w9CYLag8) failed!", +"Here it's: ptok:v1:_aWRbsN5VB9K61ABl7eN52wywzA1JU0kHB1w9CYLag8 failed!", +"-ptok:v1:_aWRbsN5VB9K61ABl7eN52wywzA1JU0kHB1w9CYLag8", +".ptok:v1:_aWRbsN5VB9K61ABl7eN52wywzA1JU0kHB1w9CYLag8", +"ptok:v1:kRIP0Oc8sUQzmwjZ2ju_0DG7sPYsSbMplGr3IpkgdEA", +"ptok:v1:QGj0Qfq3ti-UT28uqfOsofZyqMb3HscxUFds5M6-1hM", +"ptok:v1:Z8KistoEjGGA6sr8Z9AldbgUYQQC_bEfUHTspNUByhU", +"ptok:v1:at70G4JsvJ30mAKBl53VcNTUFi8FYK0W6puL5-mefNc", +"token was ptok:v1:CBoJ8vE3I9u5CvS1CbI4JvXxtn4kSPZrLl7ToDtKjFo and more", +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +"", +"error: client_secret=\"[SECRET-MASKED]\" rejected", +"", +"x", +"{\"password\": \"[SECRET-MASKED]}", +"{\"id\": 1, \"password\": \"[SECRET-MASKED] x", +"password=[SECRET-MASKED], status=ok", +"password=[SECRET-MASKED];", +"password=[SECRET-MASKED]'", +"password=ptok:v1:dr-HgNJ4UDYqKoC3DRkGk0Omu6F_noyGV_qv7IPOkck\",4111111111111111&x=1", +"password=[SECRET-MASKED]\":cd&x=1", +"password=\"[SECRET-MASKED]\"&x=1", +"password=[SECRET-MASKED]&x=1", +"{\"xml\": \"\"}", +"{\"note\": \"password=\\\"[SECRET-MASKED]\\\"&x=1\"}", +"{\"note\": \"password=[SECRET-MASKED]&x=1\"}", +"{\"password\": \"[SECRET-MASKED]\"}", +"{\"url\": \"https://x?secret=[SECRET-MASKED]\"}", +"{\"password\": \"[SECRET-MASKED]\"}", +"could not connect postgresql://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@localhost:5432/db", +"amqp://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@10.0.0.5//", +"GET https://ptok:v1:XNqMZh8_iZ4x3AH-pJfWEYF2XEjwSE8Jy6Hpw0kzvfU:@api.host/ failed", +"redis://:ptok:v1:TWnXyioKUufAQBQI4vDrZz31kmYDMo7i361DTzsYvVQ@cache:6379/0 refused", +"{\"dsn\": \"mongodb+srv://ptok:v1:3WnZ59Xqf2mMLZBvWq7gO76gJtmvr_nfOIF6R72WfJQ:ptok:v1:4EFf80ox7WcRxu_z-w_VnRFg9KnlNoO979cmu96PuxQ@cluster0.example.net/app\"}", +"fetch https://ptok:v1:6n-pFEJVVwxWmu2_xyBdOUs1aJOAJP177HYqFGRgX7c:ptok:v1:6v1czs-cr9ln5giu3wHsk9fPNC-TOXDbgiu5dhICHoA@api.host.example/v1/pay", +"amqp://ptok:v1:4kf5kuCKD3JuIoIlyxnv-CnLf3MgtB-toB95NX0xM0w:ptok:v1:EOdvSQI94XP5F1uDI01KpMTg5Go2zoWZuCYkVOCRlPY@mq:5672/", +"amqp://ptok:v1:Jzk0v_fK13PuB3KgethJ2JmmdjZjm06L6vYscLDVwdA:ptok:v1:UpWqd45iNzs8JfIp2AfeTBreV2LXkfZK_NyUoo_qch4@mq:5672/", +"sent {\"password\": \"ptok:v1:ACviYdsWVCvlWaJzzKIvSwsM3M5gbAOH6xO6aPy_c7I\"} to the gateway", +"login with password=ptok:v1:ACviYdsWVCvlWaJzzKIvSwsM3M5gbAOH6xO6aPy_c7I failed", +"retried with Bearer ptok:v1:ACviYdsWVCvlWaJzzKIvSwsM3M5gbAOH6xO6aPy_c7I", +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +"card 411111******1111 cvv [CVV-MASKED]", +"cvv=[CVV-MASKED] for 411111******1111", +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +"TRANPORTAL123ptok:v1:AQ6W6lJiUPhhfswZOx3SLOQ5DZ_fAoBspUjCrPV_4lA110.000TRK1{\"paymentData\":\"[SAD-MASKED]\"}", +"TRANPORTAL123ptok:v1:AQ6W6lJiUPhhfswZOx3SLOQ5DZ_fAoBspUjCrPV_4lA810.000202612345TRK1TrackID", +"TRANPORTAL123ptok:v1:AQ6W6lJiUPhhfswZOx3SLOQ5DZ_fAoBspUjCrPV_4lA25.000202612345TRK1TrackID", +"{\"session_id\": \"21a2496f8b6c193ec61b8dd52cbe8d827b4eaa49\", \"order_no\": \"ORD-2026-000123\", \"amount\": \"12.500\", \"currency_code\": \"KWD\", \"customer_email\": \"ptok:v1:eMwFkFHAafEM2yUlbFIkhBKSUhHqDcNpxE3bLyWFI54\", \"customer_phone\": \"ptok:v1:kVcmxuM7xSNaFxT-GvmeIwsb_FQMPsR72OBNUiZEAQc\", \"customer_first_name\": \"Jane\", \"billing_address\": {\"line1\": \"Block 1, Street 2\", \"city\": \"Kuwait City\", \"country\": \"KW\"}, \"pg_codes\": [\"mpgs-direct\", \"cs-direct\", \"kpay\"], \"redirect_url\": \"https://merchant.example/return?ref=abc123&lang=en\", \"webhook_url\": \"https://merchant.example/hooks/ottu\", \"extra\": {\"note\": \"gift wrap\", \"items\": [{\"sku\": \"SKU-0\", \"qty\": 0}, {\"sku\": \"SKU-1\", \"qty\": 1}, {\"sku\": \"SKU-2\", \"qty\": 2}, {\"sku\": \"SKU-3\", \"qty\": 3}, {\"sku\": \"SKU-4\", \"qty\": 4}, {\"sku\": \"SKU-5\", \"qty\": 5}, {\"sku\": \"SKU-6\", \"qty\": 6}, {\"sku\": \"SKU-7\", \"qty\": 7}, {\"sku\": \"SKU-8\", \"qty\": 8}, {\"sku\": \"SKU-9\", \"qty\": 9}]}, \"card_acceptance_criteria\": {\"min_expiry_time\": 30}, \"three_ds\": {\"directory_server_transaction_id\": \"3d85c5d3-8412-4f1d-9e26-4a0f00123456\"}}", +null, +"[SAD-MASKED]ptok:v1:q-aBDA-3PLgNB0wyeH6tCsUcbaLAm6sTRaHYrmp7nGI]", +"https://[SECRET-MASKED]@h.example/", +"[the cvv is ptok:v1:kVcmxuM7xSNaFxT-GvmeIwsb_FQMPsR72OBNUiZEAQc", +"123-45-6789350 mshttps://[SECRET-MASKED]@h.example/[SAD-MASKED],", +"Bearer ptok:v1:m3pR4Qyf7bWavU6TrWgyJdzV4l3KWGZAnxsT77Eiz0k\": \"", +null, +"Authorization: ptok:v1:4EFf80ox7WcRxu_z-w_VnRFg9KnlNoO979cmu96PuxQ[)", +null, +"12/27abc123\"{{,ptok:v1:UoGOZOZipLeBOQc4K2s1IUU09YK1XVLK7WXRdS7HO-o cvv is )", +"(ptok:v1:uWivpUz4ccwadzczzDqfbKX8Hc8q0iHea90LngYZxRY+96550000000token=ptok:v1:q-aBDA-3PLgNB0wyeH6tCsUcbaLAm6sTRaHYrmp7nGI,{\"[SAD-MASKED]\"12/27", +"}cvv https://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@h/"", +null, +"https://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@h/abc123cvv=:cvv |", +null, +"ptok:v1:q-aBDA-3PLgNB0wyeH6tCsUcbaLAm6sTRaHYrmp7nGI\\", +null, +"350 ms\\token=ptok:v1:fUFDP3wpQT6UVBtFrk6CbAfbhwq1z-qOuGJea1ro8Ls}password=}{Authorization: \"ptok:v1:r2HMq_CFEIsYZySf_MlUxRN1WoQRk0vAmUnzLSPnyDA\": \"", +"1234Bearer \"https://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@h/[SAD-MASKED]\"token\": \"", +"[CVV-MASKED](", +"200\"[SAD-MASKED]\"12/27)jane@example.com123411111111111111112/27200", +null, +null, +"123[cvv=,ptok:v1:kVcmxuM7xSNaFxT-GvmeIwsb_FQMPsR72OBNUiZEAQc", +",(\"[SAD-MASKED]\"Authorization: ptok:v1:1K9jSW00oRl0vDLwnRWeiTcOCahAMdlJcmWGIvX62Uc>", +"GB33BUKB20201555555555,\"token\": \"ptok:v1:EsBQJIpLdLMq803a3BbrDb_FmNJIAGjT09wyCeLZ7OE>", +"\"[SAD-MASKED]\"https://[SECRET-MASKED]@h.example/ (350 msabc123\"350 mspassword=", +"\\https://[SECRET-MASKED]@h.example/", +",["the cvv is ptok:v1:uWivpUz4ccwadzczzDqfbKX8Hc8q0iHea90LngYZxRY", +"https://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@h/411111******1111\"token\": \"}"the cvv is Bearer password=", +"(cvv [SAD-MASKED]cvv=ptok:v1:q-aBDA-3PLgNB0wyeH6tCsUcbaLAm6sTRaHYrmp7nGI:ptok:v1:uWivpUz4ccwadzczzDqfbKX8Hc8q0iHea90LngYZxRY", +"ptok:v1:kVcmxuM7xSNaFxT-GvmeIwsb_FQMPsR72OBNUiZEAQccvv=350 ms", +null, +"{password=ptok:v1:5pID2SxgTGXwD6lym8bDTA4DjsmCC_0pZ9Pz4otwJ0A cvv is GB33BUKB20201555555555{abc123123-45-6789123-45-6789", +"[SAD-MASKED]password='}", +null, +null, +"]ptok:v1:q-aBDA-3PLgNB0wyeH6tCsUcbaLAm6sTRaHYrmp7nGI(ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\"https://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@h/]", +"Authorization: ptok:v1:Zekuypmh6yaS1AsQvDsvTlyFOrYxCFerqu1nVu19pvM", +"\"token\": \"ptok:v1:OgntccXlyQyW_nL_x0YrSu-X58RMe9zvBVdgzO8rDNA&password=ptok:v1:DvQKTJwXw4I8OCsOnchudoJOZPrAqtbZJvgg7g9bCy8(Authorization: ptok:v1:evPjFdIK1Ne_NCz6OPyEabtAWlfyfdmpbnmxKeU6Gpc", +"123|(4111111111111111cvv [CVV-MASKED]", +"|123-45-6789}cvv=the cvv is [SAD-MASKED] 200", +"|https://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@h/\"[SAD-MASKED]\"{", +"{\"[SAD-MASKED]\"abc123", +null, +":\"[SAD-MASKED]\"cvv=https://[SECRET-MASKED]@h.example/payment_id=abc12345the cvv is ptok:v1:uWivpUz4ccwadzczzDqfbKX8Hc8q0iHea90LngYZxRY", +null, +null, +null, +"|'(:)[SAD-MASKED]{[ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", +":\"[SAD-MASKED]\"}123412/27200GB33BUKB20201555555555'", +null, +"\"token\": \"ptok:v1:5RkPBwJmQpSnPW3s96Hx64R40DfXrcgCOC7_t9KkkJk[SAD-MASKED]4111111111111111}123-45-6789", +"cvv [CVV-MASKED]\"[SAD-MASKED]\"|12/27{GB33BUKB20201555555555350 ms", +"+96550000000token=Authorization: ptok:v1:ZiIw3nqLLx-8X4kmmh8eycCZAFu3Vw8KPm3lu36qCvw cvv is \"token\": \"", +null, +null, +"Bearer 350 ms123-45-6789:ptok:v1:aYJdn4WHn22T9DxSOhz71PawxIAImL3uKKs0MVy3nMM ", +"GB33BUKB20201555555555(https://[SECRET-MASKED]@h.example/:]ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA123412/27the cvv is ", +"cvv token=ptok:v1:PReRrfIu9TdFf__-XxasmlOr2g-HkkQD2g2NsB84Zo8", +null, +null, +"123411111******1111GB33BUKB20201555555555abc123123\"[SAD-MASKED]\"cvv ", +null, +"ptok:v1:kVcmxuM7xSNaFxT-GvmeIwsb_FQMPsR72OBNUiZEAQc", +"https://[SECRET-MASKED]@h.example/123the cvv is }", +null, +"'s3cr3thttps://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@h/[password=[SECRET-MASKED]:'", +"password=ptok:v1:1l8n_y53HKMEMBLPO5ry92SiSdJvJ-cjdnCeJ3LWuHA cvv='jane@example.comGB33BUKB20201555555555", +null, +null, +"]411111******1111123-45-6789cvv=4111111111111111[SAD-MASKED])", +null, +null, +"|ptok:v1:uWivpUz4ccwadzczzDqfbKX8Hc8q0iHea90LngYZxRY", +"\\Bearer ptok:v1:KWa4pfUdM_6WCxc2xeXqE1Ajzv1o7IqWQb6h5p-O8tw\": \"]", +null, +null, +null, +null, +""cvv [CVV-MASKED]\"[SAD-MASKED]\"\"[SAD-MASKED]\"\"Authorization: ", +"ptok:v1:DvQKTJwXw4I8OCsOnchudoJOZPrAqtbZJvgg7g9bCy8abc123,200350 ms\"token\": \"[", +"[Bearer ptok:v1:5MRTSHp-k2OJDjxTiM32aAz00okDFywixktAPKMGjG0", +"\\123-45-6789https://u:p@h/https://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@h/", +null, +"&350 ms&',Authorization: ptok:v1:XF_u3vLQRcTylgYgpdOmueKaFPsDokaheBcKp4DZH40", +"&123-45-6789Bearer https://[SECRET-MASKED]@h.example/]200jane@example.comGB33BUKB20201555555555abc123", +")ptok:v1:UoGOZOZipLeBOQc4K2s1IUU09YK1XVLK7WXRdS7HO-o cvv is ", +"}ptok:v1:uWivpUz4ccwadzczzDqfbKX8Hc8q0iHea90LngYZxRY", +null, +"\"[SAD-MASKED]\"411111******1111'[the cvv is payment_id=abc12345token=token=", +null, +null, +null, +"jane@example.com350 ms\"[SAD-MASKED]\"\"ptok:v1:uWivpUz4ccwadzczzDqfbKX8Hc8q0iHea90LngYZxRY&", +" ptok:v1:q-aBDA-3PLgNB0wyeH6tCsUcbaLAm6sTRaHYrmp7nGI+96550000000cvv=&]", +"\"ptok:v1:kVcmxuM7xSNaFxT-GvmeIwsb_FQMPsR72OBNUiZEAQc[SAD-MASKED]|[SAD-MASKED] (", +null, +"\"token=ptok:v1:0uZY6ffYA3KpsryjU3MqEO7mfbMhE-bQ5ZSkj4BAO4A>ptok:v1:DvQKTJwXw4I8OCsOnchudoJOZPrAqtbZJvgg7g9bCy8", +"123-45-678912/27eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.c2lnptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[SAD-MASKED]\"token\": \"[SECRET-MASKED]", +null, +"+96550000000cvv ptok:v1:uWivpUz4ccwadzczzDqfbKX8Hc8q0iHea90LngYZxRY\\https://[SECRET-MASKED]@h.example/", +null, +"[SAD-MASKED]", +null, +"Authorization: ptok:v1:2Oeil8LAqV8Xn3MXo2YVevUtJxAGGywqfk7alslJ5rw ms", +"}|ptok:v1:uWivpUz4ccwadzczzDqfbKX8Hc8q0iHea90LngYZxRY", +"]350 ptok:v1:TCzvVbUyYQDxRU4pNFPDFOlsq2cEejMmhK351T9L1Xg=s3cr3teyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.c2ln", +"GB33BUKB20201555555555|ptok:v1:7EOdctokf-ZR8FCj6iYp_odRy_RG0CWvKp6c5SAfXeA@example.com123+96550000000+96550000000[", +" 123-45-6789&abc123ptok:v1:8MZB0mw2lBRaQkk0bEpv4gXCa3W9TPfz3_JdOnvhkxI:", +"(token=ptok:v1:RRa_cZrQaFwR42E7-XYXj212GHHF43_9AnNancpWMbc>(password=the cvv is GB33BUKB20201555555555", +null, +"200https://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@h/ the cvv is https://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@h/ptok:v1:DvQKTJwXw4I8OCsOnchudoJOZPrAqtbZJvgg7g9bCy8)password=", +"Bearer ptok:v1:ovaaSTqLBhB0cUpknJOicJd2BNk4oIpGlOZkFXB5vrA[SAD-MASKED]", +null, +"[SAD-MASKED]|411111******1111{[SECRET-MASKED]payment_id=abc12345411111******1111token=", +"12/27\"[SAD-MASKED]\"350 ms123-45-6789[", +"\"[SAD-MASKED]\"123-45-6789the cvv is ]", +null, +"'the cvv is https://[SECRET-MASKED]@h.example/"cvv=", +"ptok:v1:_dsmHy3z6GB3FHbxNVN2jIp_YMk1kjcK6ytMHqqdxmU=)", +null, +null, +null, +"token=\"[SAD-MASKED]\"token=", +"': ptok:v1:q-aBDA-3PLgNB0wyeH6tCsUcbaLAm6sTRaHYrmp7nGI{https://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@h/", +"200&:\"[SAD-MASKED]\""411111******1111the cvv is \"[SAD-MASKED]\"", +null, +"https://[SECRET-MASKED]@h.example/[cvv s3cr3t\"", +":(|the cvv is password=ptok:v1:lKdUygWS17casw0zJquaJaqTCHO-h9CgXlDbiNRccjw>", +null, +null, +"ptok:v1:DvQKTJwXw4I8OCsOnchudoJOZPrAqtbZJvgg7g9bCy8'(\\https://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@h/&", +null, +null, +" (password=ptok:v1:8fS9A2OE2Up7bcxCCyRtQpsesC7JmTY53o50FtfSj4Y ", +",ptok:v1:uWivpUz4ccwadzczzDqfbKX8Hc8q0iHea90LngYZxRY", +"]|\"token\": \"ptok:v1:FvClcU0VwPVODVVe5NWLakuHz4bZ4fWR6U0U7ZHLJrA,|123-45-6789payment_id=abc12345", +"{ptok:v1:DvQKTJwXw4I8OCsOnchudoJOZPrAqtbZJvgg7g9bCy8\"token\": \"ptok:v1:sRxfpE-CGkxp9cDGZ5HO2EYXm5nbKPDEpUQYE49PYLA cvv=}200", +"12/27\\|https://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@h/GB33BUKB20201555555555\"token\": \"", +null, +null, +"Authorization: ptok:v1:YO091qKopha2KD9PBKapsFeE3DR1AJQe-BuTP4Vs1tc", +"123 ptok:v1:HLmb8YzgoK1U35ZfKaZGFc_r95nm6aLiKq6kqALW8sY=", +"411111******1111s3cr3t1234}cvv ,}cvv=ptok:v1:kVcmxuM7xSNaFxT-GvmeIwsb_FQMPsR72OBNUiZEAQc", +"123ptok:v1:DvQKTJwXw4I8OCsOnchudoJOZPrAqtbZJvgg7g9bCy8cvv password="s3cr3thttps://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@h/token=ptok:v1:Q_I98ohtQOorY_mzbIOseTxSA4OJvtRld_pkqXhuMWo", +null, +":payment_id=abc12345}https://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@h/password=\"", +"123-45-6789https://ptok:v1:ost6aWhAO6NKoT1x843aTwXQj5t60xzRGKq-wZkqjl0/411111******1111", +"ptok:v1:q-aBDA-3PLgNB0wyeH6tCsUcbaLAm6sTRaHYrmp7nGI\"[SAD-MASKED]\" ", +"ptok:v1:DvQKTJwXw4I8OCsOnchudoJOZPrAqtbZJvgg7g9bCy8]\"token\": \"&abc123\\the cvv is the cvv is 123-45-6789(", +"\\ptok:v1:q-aBDA-3PLgNB0wyeH6tCsUcbaLAm6sTRaHYrmp7nGI,cvv=,)", +null, +"'350 mshttps://[SECRET-MASKED]@h.example/'\\the cvv is ", +"https://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@h/password=\"ptok:v1:x5tD1gwqkgd68kO_KuLlX_HmFctvM8NGcKM90JidqYA&123", +"\\cvv \"[SAD-MASKED]\"", +null, +"https://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@h/ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAuthorization: 4111111111111111123412/27Bearer [SECRET-MASKED]", +": GB33BUKB20201555555555eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.c2ln ptok:v1:DvQKTJwXw4I8OCsOnchudoJOZPrAqtbZJvgg7g9bCy8{ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", +"\",cvv |abc123ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA123-45-6789ptok:v1:DvQKTJwXw4I8OCsOnchudoJOZPrAqtbZJvgg7g9bCy8", +null, +null, +": ]cvv=https://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@h/", +"\"[SAD-MASKED]\")}", +"https://[SECRET-MASKED]@h.example/:the cvv is Bearer &the cvv is \"ptok:v1:q-aBDA-3PLgNB0wyeH6tCsUcbaLAm6sTRaHYrmp7nGI+96550000000", +null, +"password=ptok:v1:nPAg-_KQLK8gaimtL3BFCAwg8tnfVdZQjoPiXotRl7s],password=", +null, +"12/27Bearer 123Authorization: password=ptok:v1:OgntccXlyQyW_nL_x0YrSu-X58RMe9zvBVdgzO8rDNA[SECRET-MASKED]abc123Bearer payment_id=abc12345", +null, +null, +null, +null, +"token=ptok:v1:ZiIw3nqLLx-8X4kmmh8eycCZAFu3Vw8KPm3lu36qCvw cvv is ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAhttps://[SECRET-MASKED]@h.example/Bearer cvv ", +null, +"\\)\"[SAD-MASKED]\"\\the cvv is ptok:v1:DvQKTJwXw4I8OCsOnchudoJOZPrAqtbZJvgg7g9bCy8", +null, +"|the cvv is https://[SECRET-MASKED]@h.example/ cvv ", +"ptok:v1:q-aBDA-3PLgNB0wyeH6tCsUcbaLAm6sTRaHYrmp7nGI'\"|\"4111111111111111abc1231234s3cr3t", +null, +"Authorization: ptok:v1:DrCM8xehZjcWgStLT0YD4-0mHb3ezHcqvI4qH3-3Ndk&", +"payment_id=abc12345ptok:v1:uWivpUz4ccwadzczzDqfbKX8Hc8q0iHea90LngYZxRY\"[SAD-MASKED]\"https://[SECRET-MASKED]@h.example/Bearer \\", +null, +"Authorization: [SECRET-MASKED][SAD-MASKED]"&", +null, +"[SAD-MASKED]200s3cr3t123", +"(ptok:v1:DvQKTJwXw4I8OCsOnchudoJOZPrAqtbZJvgg7g9bCy8]password=ptok:v1:X7nJGQEn9vf8dLqRkv5ByDkGcSNP_64tcOykdQFsK3Yhttps://[SECRET-MASKED]@h.example/", +null, +null, +"\"token\": \"ptok:v1:VBlRNHeBVAYTDo1JiNzrMka8V9J7Ud_zIWudwd4nMAI ms12/27350 mseyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.c2ln", +"&+96550000000]https://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@h/4111111111111111{\"", +"ptok:v1:kVcmxuM7xSNaFxT-GvmeIwsb_FQMPsR72OBNUiZEAQc\"[SECRET-MASKED]'s3cr3t\"4111111111111111GB33BUKB20201555555555s3cr3t[SECRET-MASKED]", +"https://[SECRET-MASKED]@h.example/'{\\", +null, +null, +"ptok:v1:WA-jniwe6G3jZGnRKp8UvRjIQaUmo9pxBgxlCIDYzQs ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA350 msAuthorization: https://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@h/411111******1111", +null, +"[,Authorization: ptok:v1:uLmgaYjqjKRFlj5_KrJvP4jRH_4srfk8Mww59yXyg-4 ", +"411111******1111ptok:v1:DvQKTJwXw4I8OCsOnchudoJOZPrAqtbZJvgg7g9bCy8200cvv 4111111111111111\"12/27https://u:p@h/", +"the cvv is (s3cr3t'https://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@h/ptok:v1:UoGOZOZipLeBOQc4K2s1IUU09YK1XVLK7WXRdS7HO-o cvv is \"token\": \"ptok:v1:jiUXyXo6tuUE0YobQ2OqEoMLzftfJ1u-h0lDOrSPYRM[SAD-MASKED]", +"\"ptok:v1:q-aBDA-3PLgNB0wyeH6tCsUcbaLAm6sTRaHYrmp7nGI:411111******1111", +null, +"'cvv=ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA350 mshttps://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@h/ https://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@h/123", +"{the cvv is ptok:v1:WA-jniwe6G3jZGnRKp8UvRjIQaUmo9pxBgxlCIDYzQs=200"200}", +"[SAD-MASKED]\\\\", +"ptok:v1:DvQKTJwXw4I8OCsOnchudoJOZPrAqtbZJvgg7g9bCy8 ]{s3cr3t]|[SAD-MASKED]", +null, +"411111******1111])}https://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@h/", +"https://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@h/Authorization: ptok:v1:sRxfpE-CGkxp9cDGZ5HO2EYXm5nbKPDEpUQYE49PYLA |[SECRET-MASKED]200350 ms", +"20041111111111111114111111111111111\"ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAhttps://ptok:v1:ost6aWhAO6NKoT1x843aTwXQj5t60xzRGKq-wZkqjl0/ \"[SAD-MASKED]\"{", +null, +"411111******1111eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.c2ln,\"[SAD-MASKED]\"GB33BUKB20201555555555|\\", +null, +"Authorization: )'('ptok:v1:DvQKTJwXw4I8OCsOnchudoJOZPrAqtbZJvgg7g9bCy8the cvv is the cvv is "", +"][SAD-MASKED]&", +"Authorization: ,https://[SECRET-MASKED]@h.example/", +"1234cvv=password=\"ptok:v1:r2HMq_CFEIsYZySf_MlUxRN1WoQRk0vAmUnzLSPnyDA\": \"", +"12/27\"[SAD-MASKED]\"Authorization: ", +null, +"{Authorization: 200(ptok:v1:DvQKTJwXw4I8OCsOnchudoJOZPrAqtbZJvgg7g9bCy8ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", +null, +null, +"https://[SECRET-MASKED]@h.example/cvv=ptok:v1:bPyqqfjBrHW8EncMmO_6a1DVXuNLbIfPPSzq78vuyNA/27token=12/27", +"]200https://ptok:v1:ost6aWhAO6NKoT1x843aTwXQj5t60xzRGKq-wZkqjl0/350 ms"", +null, +null, +""https://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@h/|ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", +"password=ptok:v1:DvQKTJwXw4I8OCsOnchudoJOZPrAqtbZJvgg7g9bCy8", +null, +null, +" (1234ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAtoken=\"[SAD-MASKED]\"cvv=jane@example.com200", +"[https://[SECRET-MASKED]@h.example/ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA411111******1111\"]", +null, +"\"token\": \"ptok:v1:y6ZP5NUfByC2a86RgdgXOVXw2uzjEsgp1L_9mHaqLe4\" [ptok:v1:DvQKTJwXw4I8OCsOnchudoJOZPrAqtbZJvgg7g9bCy8token=1234", +""123\"token\": \"ptok:v1:QAG_WrCjrJnyj1IJ-PHGRcovK_xSKvRZW40eB_-kjnA ", +"Bearer https://[SECRET-MASKED]@h.example/'", +"GB33BUKB20201555555555\"[SAD-MASKED]\"https://[SECRET-MASKED]@h.example/\"token\": \"ptok:v1:1oqExnXK2I7lN5eJQl5EVtK1CKwLJM2ji6XHy3taI-I}ptok:v1:DvQKTJwXw4I8OCsOnchudoJOZPrAqtbZJvgg7g9bCy8abc123", +null, +null, +"Bearer ptok:v1:kmoFHE5jSsFXz2p_MwlYWKeaUtHAiI60F99guNncwB8>\\]\\", +null, +"https://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@h/cvv cvv [SAD-MASKED]'", +"[SECRET-MASKED]&4111111111111111&https://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@h/\\[SECRET-MASKED]", +"}"cvv=][SECRET-MASKED][SECRET-MASKED]token=ptok:v1:MyMjFior5uA-BjPeGQy76ee0H5JiRSnT-yVjTBcKmWk 4111111111111111", +"ptok:v1:DvQKTJwXw4I8OCsOnchudoJOZPrAqtbZJvgg7g9bCy8", +")https://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@h/", +null, +"[SAD-MASKED]cvv ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAptok:v1:UoGOZOZipLeBOQc4K2s1IUU09YK1XVLK7WXRdS7HO-o cvv is }abc123", +"Authorization: ptok:v1:qy2TOKFCtjNvRVgeGRo8qp2-K7jO9x9Kyf5SiWSi-6M", +"cvv token=GB33BUKB20201555555555411111******1111https://ptok:v1:ost6aWhAO6NKoT1x843aTwXQj5t60xzRGKq-wZkqjl0/:ptok:v1:d-E1BGXQVy1Ib2StLw3dVWAO_eXiossJOGOwgXkxHUg", +"\"[SAD-MASKED]\"cvv cvv=[SECRET-MASKED]jane@example.com411111******1111", +"s3cr3t[SAD-MASKED]cvv=\\1234", +"Bearer (\"[SAD-MASKED]\"", +"123-45-6789{[ptok:v1:q-aBDA-3PLgNB0wyeH6tCsUcbaLAm6sTRaHYrmp7nGI:Bearer "", +null, +"GB33BUKB20201555555555123\"token\": \"ptok:v1:OgntccXlyQyW_nL_x0YrSu-X58RMe9zvBVdgzO8rDNA[SAD-MASKED][SAD-MASKED],"]", +"https://[SECRET-MASKED]@h.example/token=ptok:v1:lyZxRgpOd9pLhGzGaBIdWyXkxCP6CisOOxLl8Pb6TNs ms]https://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@h/\"|{password=", +"[&123-45-6789411111******1111+96550000000[SAD-MASKED]abc123GB33BUKB20201555555555", +"[SECRET-MASKED]4111111111111111"cvv=ptok:v1:uWivpUz4ccwadzczzDqfbKX8Hc8q0iHea90LngYZxRYptok:v1:UoGOZOZipLeBOQc4K2s1IUU09YK1XVLK7WXRdS7HO-o cvv is ", +"[SECRET-MASKED]"Authorization: \"ptok:v1:r2HMq_CFEIsYZySf_MlUxRN1WoQRk0vAmUnzLSPnyDA\": \"", +null, +"cvv [CVV-MASKED][SAD-MASKED] Authorization: ptok:v1:Zekuypmh6yaS1AsQvDsvTlyFOrYxCFerqu1nVu19pvM", +null, +"\"[SAD-MASKED]\"&", +"Bearer }s3cr3tBearer [CVV-MASKED]", +"1234[SECRET-MASKED]\"[SAD-MASKED]\"ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAthe cvv is ", +"\"[SAD-MASKED]\"the cvv is 411111******1111'", +null, +null, +null, +"ptok:v1:q-aBDA-3PLgNB0wyeH6tCsUcbaLAm6sTRaHYrmp7nGI,", +"ptok:v1:uWivpUz4ccwadzczzDqfbKX8Hc8q0iHea90LngYZxRY\"[SECRET-MASKED]https://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@h/", +"|"&ptok:v1:DvQKTJwXw4I8OCsOnchudoJOZPrAqtbZJvgg7g9bCy8]350 ms123-45-6789", +"Bearer \"[SAD-MASKED]\"200\"[SAD-MASKED]\",:ptok:v1:kVcmxuM7xSNaFxT-GvmeIwsb_FQMPsR72OBNUiZEAQc}abc123", +"token=ptok:v1:vCFcGO1n1078u72CH6TnfJ8c9hwfe3dYnuZObOjbUTE", +null, +"(https://[SECRET-MASKED]@h.example/]ptok:v1:uWivpUz4ccwadzczzDqfbKX8Hc8q0iHea90LngYZxRY", +"Authorization: [CVV-MASKED]", +"(\"[SAD-MASKED]\"41111111111111111234payment_id=abc12345", +null, +")}https://[SECRET-MASKED]@h.example/ptok:v1:DvQKTJwXw4I8OCsOnchudoJOZPrAqtbZJvgg7g9bCy8https://u:p@h/'ptok:v1:q-aBDA-3PLgNB0wyeH6tCsUcbaLAm6sTRaHYrmp7nGI[SECRET-MASKED]+96550000000123-45-6789", +"\"[SAD-MASKED]\"200eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.c2ln[SECRET-MASKED]cvv ", +" the cvv is 411111******1111\"}Bearer [SAD-MASKED]", +"}Bearer token=ptok:v1:XSAoE2BYJiC441djdmhmP6xIx8exKNz96Pq-VSyzBdI)", +null, +"s3cr3ttoken=ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[SAD-MASKED]cvv=,password=", +null, +"123-45-6789200)s3cr3t(Bearer ptok:v1:jlxBgofBIDCRgJbdmZdbVJt0XeIcmW4UC3alu939-ic", +"4111111111111111123-45-6789[1234password= cvv=token=\"ptok:v1:OgntccXlyQyW_nL_x0YrSu-X58RMe9zvBVdgzO8rDNA", +"https://[SECRET-MASKED]@h.example/ptok:v1:q-aBDA-3PLgNB0wyeH6tCsUcbaLAm6sTRaHYrmp7nGI", +null, +"abc123https://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@h/Bearer &(", +"[SAD-MASKED]]\"Authorization: ptok:v1:YgbfcFSHZJWtVqdJ5WJj1XbbjlVuK6iakCU4Ub9wo2w" +], +"keyset/every-pack/events":[ +{ +"event":"api request received: POST /b/checkout/v1/pymt-txn/", +"http":{ +"request":{ +"body":"{\"session_id\": \"21a2496f8b6c193ec61b8dd52cbe8d827b4eaa49\", \"order_no\": \"ORD-2026-000123\", \"amount\": \"12.500\", \"currency_code\": \"KWD\", \"customer_email\": \"ptok:v1:eMwFkFHAafEM2yUlbFIkhBKSUhHqDcNpxE3bLyWFI54\", \"customer_phone\": \"ptok:v1:kVcmxuM7xSNaFxT-GvmeIwsb_FQMPsR72OBNUiZEAQc\", \"customer_first_name\": \"ptok:v1:NVSAuFYU9wWRKK_cQt9wbZTw0HOXuh5NgHiMxgv64o8\", \"billing_address\": {\"line1\": \"ptok:v1:Ts0ReoBNKiJBYHXTW1WbXX7iUnDW6VwUwCUjR_LLNNY\", \"city\": \"ptok:v1:fKrE4cXxHekZk4mklS_6gF9X89UW19yWC8H9AaftrcE\", \"country\": \"ptok:v1:9hw5Sp-i6dZADYPdeIHwEOV3awFcLyE-i2AgY5JGTGs\"}, \"pg_codes\": [\"mpgs-direct\", \"cs-direct\", \"kpay\"], \"redirect_url\": \"https://merchant.example/return?ref=abc123&lang=en\", \"webhook_url\": \"https://merchant.example/hooks/ottu\", \"extra\": {\"note\": \"gift wrap\", \"items\": [{\"sku\": \"SKU-0\", \"qty\": 0}, {\"sku\": \"SKU-1\", \"qty\": 1}, {\"sku\": \"SKU-2\", \"qty\": 2}, {\"sku\": \"SKU-3\", \"qty\": 3}, {\"sku\": \"SKU-4\", \"qty\": 4}, {\"sku\": \"SKU-5\", \"qty\": 5}, {\"sku\": \"SKU-6\", \"qty\": 6}, {\"sku\": \"SKU-7\", \"qty\": 7}, {\"sku\": \"SKU-8\", \"qty\": 8}, {\"sku\": \"SKU-9\", \"qty\": 9}]}, \"card_acceptance_criteria\": {\"min_expiry_time\": 30}, \"three_ds\": {\"directory_server_transaction_id\": \"ptok:v1:LXiC7pzkZj27hoZISoe9Fas88oeB2ULx3PiqcFJv3v8\"}}", +"headers":{ +"authorization":"ptok:v1:s1-vTKcgCkGB8g8AIHMP2e9PJegX0mQGHGVg1OC2-UI", +"user-agent":"Mozilla/5.0" +} +} +}, +"payload":{ +"amount":"12.500", +"billing_address":{ +"city":"ptok:v1:fKrE4cXxHekZk4mklS_6gF9X89UW19yWC8H9AaftrcE", +"country":"ptok:v1:9hw5Sp-i6dZADYPdeIHwEOV3awFcLyE-i2AgY5JGTGs", +"line1":"ptok:v1:Ts0ReoBNKiJBYHXTW1WbXX7iUnDW6VwUwCUjR_LLNNY" +}, +"card_acceptance_criteria":{ +"min_expiry_time":30 +}, +"currency_code":"KWD", +"customer_email":"ptok:v1:eMwFkFHAafEM2yUlbFIkhBKSUhHqDcNpxE3bLyWFI54", +"customer_first_name":"ptok:v1:NVSAuFYU9wWRKK_cQt9wbZTw0HOXuh5NgHiMxgv64o8", +"customer_phone":"ptok:v1:kVcmxuM7xSNaFxT-GvmeIwsb_FQMPsR72OBNUiZEAQc", +"extra":{ +"items":[ +{ +"qty":0, +"sku":"SKU-0" +}, +{ +"qty":1, +"sku":"SKU-1" +}, +{ +"qty":2, +"sku":"SKU-2" +}, +{ +"qty":3, +"sku":"SKU-3" +}, +{ +"qty":4, +"sku":"SKU-4" +}, +{ +"qty":5, +"sku":"SKU-5" +}, +{ +"qty":6, +"sku":"SKU-6" +}, +{ +"qty":7, +"sku":"SKU-7" +}, +{ +"qty":8, +"sku":"SKU-8" +}, +{ +"qty":9, +"sku":"SKU-9" +} +], +"note":"gift wrap" +}, +"order_no":"ORD-2026-000123", +"pg_codes":[ +"mpgs-direct", +"cs-direct", +"kpay" +], +"redirect_url":"https://merchant.example/return?ref=abc123&lang=en", +"session_id":"21a2496f8b6c193ec61b8dd52cbe8d827b4eaa49", +"three_ds":{ +"directory_server_transaction_id":"ptok:v1:LXiC7pzkZj27hoZISoe9Fas88oeB2ULx3PiqcFJv3v8" +}, +"webhook_url":"https://merchant.example/hooks/ottu" +}, +"url":{ +"full":"https://jade.ottu.dev/b/checkout/v1/pymt-txn/?utm=1" +} +}, +{ +"event":"pg.response_received from apitest.cybersource.com", +"http":{ +"request":{ +"method":"POST" +}, +"response":{ +"body":"{\"id\": \"pay_9f8e7d6c5b4a\", \"status\": \"AUTHORIZED\", \"amount\": \"100.000\", \"currency\": \"KWD\", \"processorInformation\": {\"approvalCode\": \"831000\", \"responseCode\": \"100\", \"transactionId\": \"726650******4567\"}, \"orderInformation\": {\"amountDetails\": {\"authorizedAmount\": \"100.000\", \"currency\": \"KWD\"}}, \"_links\": {\"self\": {\"href\": \"/pts/v2/payments/7266500001234567\"}}}", +"status_code":201 +} +}, +"labels":{ +"direction":"outbound", +"namespace":"cybersource", +"operation":"payment", +"pg_result":"AUTHORIZED" +}, +"merchant_id":"jade.ottu.dev", +"payment":{ +"pg_code":"cs-direct", +"reference":"jade-oTTE45" +}, +"service":{ +"target":{ +"name":"cybersource" +} +}, +"session_id":"b1e2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0", +"url":{ +"full":"https://apitest.cybersource.com/pts/v2/payments/" +} +}, +{ +"event":"pg.response_received from apitest.cybersource.com", +"http":{ +"request":{ +"method":"POST" +}, +"response":{ +"body":"{\"access_token\": \"ptok:v1:iWjJ0BTFxqptSpf9yTbBc2WW0qn3L3x0WxlYMRSohJI\", \"id\": \"pay_9f8e7d6c5b4a\", \"status\": \"AUTHORIZED\", \"amount\": \"100.000\", \"currency\": \"KWD\", \"processorInformation\": {\"approvalCode\": \"831000\", \"responseCode\": \"100\", \"transactionId\": \"726650******4567\"}, \"orderInformation\": {\"amountDetails\": {\"authorizedAmount\": \"100.000\", \"currency\": \"KWD\"}}, \"_links\": {\"self\": {\"href\": \"/pts/v2/payments/7266500001234567\"}}}", +"status_code":201 +} +}, +"labels":{ +"direction":"outbound", +"namespace":"cybersource", +"operation":"payment", +"pg_result":"AUTHORIZED" +}, +"merchant_id":"jade.ottu.dev", +"payment":{ +"pg_code":"cs-direct", +"reference":"jade-oTTE45" +}, +"service":{ +"target":{ +"name":"cybersource" +} +}, +"session_id":"b1e2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0", +"url":{ +"full":"https://apitest.cybersource.com/pts/v2/payments/" +} +}, +{ +"event":"pg.request_sent to MPGS", +"message_text":"card 411111******1111 cvv [CVV-MASKED] for ptok:v1:ncKKpBImcKIh91as3qZfSn9AhHZTyyFjBOnZfwq4xkY", +"payload":{ +"customer":{ +"email":"ptok:v1:ncKKpBImcKIh91as3qZfSn9AhHZTyyFjBOnZfwq4xkY", +"firstName":"ptok:v1:NVSAuFYU9wWRKK_cQt9wbZTw0HOXuh5NgHiMxgv64o8" +}, +"order":{ +"amount":20, +"reference":"deltabRKJ5X_0" +}, +"sourceOfFunds":{ +"provided":{ +"card":{ +"expiry":{ +"month":"01", +"year":"27" +}, +"number":"411111******1111", +"securityCode":"[CVV-MASKED]" +} +} +} +}, +"session_id":"b1e2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0" +}, +{ +"event":"create payment", +"token":"ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"cvv":"[CVV-MASKED]" +}, +{ +"cvv":"[CVV-MASKED]" +}, +{ +"session_key":"ptok:v1:8O9tqpAd1nMMT3yelPozCFaxMX7IiICgfgF3iSXjz8k" +}, +{ +"access_token":null +}, +{ +"card":null, +"customer_name":null, +"cvv":null, +"expiry":null, +"public_key":null +}, +{ +"Cvv":"[CVV-MASKED]" +}, +{ +"securityCode":"[CVV-MASKED]" +}, +{ +"data":{ +"token":{ +"more":"ptok:v1:7ZWOgZZFY3UYwHRVga6PD6VriwK0ettMsFZOMt0d2IY", +"nested":"ptok:v1:8ThbeH-dNcoVUO63vcVSzMeivjAFF4neP158xd6Czm0" +} +} +}, +{ +"payload":{ +"note":"[SAD-MASKED]", +"paymentData":"[SAD-MASKED]" +} +}, +{ +"http":{ +"request":{ +"body":{ +"content":"TRANPORTAL123ptok:v1:AQ6W6lJiUPhhfswZOx3SLOQ5DZ_fAoBspUjCrPV_4lA110.000TRK1{\"paymentData\":\"[SAD-MASKED]\"}" +} +} +} +}, +{ +"http":{ +"request":{ +"body":{ +"content":"TRANPORTAL123ptok:v1:AQ6W6lJiUPhhfswZOx3SLOQ5DZ_fAoBspUjCrPV_4lA810.000202612345TRK1TrackID" +} +} +} +}, +{ +"http":{ +"request":{ +"body":{ +"content":"TRANPORTAL123ptok:v1:AQ6W6lJiUPhhfswZOx3SLOQ5DZ_fAoBspUjCrPV_4lA25.000202612345TRK1TrackID" +} +} +} +}, +{ +"event":"ptok:v1:BCng-t9pGDNJZb8Kxr-8dbnx0vH9S1VGixXZwTjuAXw" +}, +{ +"event":"ptok:v1:fbBTZVC9H_ZhvwccVlIcAT81aJ11pahAQwMADLNidjk" +}, +{ +"event":"ptok:v1:KQvnEFp44z39aJFKt8AgvxsnNi558q_g7PpLLFmjhU8" +}, +{ +"event":"ptok:v1:IPPblK6aMP_OH1mPa_NmJEku5a8A4DNuj0e8_FswPm0" +}, +{ +"event":"ptok:v1:nMYkVx-T7gC81k-ugXTnuEVARP23rbgqh68K2StP3Fw" +}, +{ +"event":"'token': 'ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo'" +}, +{ +"event":"'token':'ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo'" +}, +{ +"event":"\"token\": \"ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo\"" +}, +{ +"event":"\"token\":\"ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo\"" +}, +{ +"event":"token: ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"token:ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"token= ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"token=ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"token=ptok:v1:7ZWOgZZFY3UYwHRVga6PD6VriwK0ettMsFZOMt0d2IY" +}, +{ +"event":"token=ptok:v1:JuuCkMnRlUJr5bdNk7w-vsvpb9mqq7UBGofK_xBj-0I" +}, +{ +"event":"token: ptok:v1:7ZWOgZZFY3UYwHRVga6PD6VriwK0ettMsFZOMt0d2IY" +}, +{ +"event":"token: ptok:v1:JuuCkMnRlUJr5bdNk7w-vsvpb9mqq7UBGofK_xBj-0I" +}, +{ +"event":"token:ptok:v1:_lXByvUrQgAiZ4EOeOXGT_PlbUd0M_GNzfg43nNBFX4" +}, +{ +"event":"token=ptok:v1:_lXByvUrQgAiZ4EOeOXGT_PlbUd0M_GNzfg43nNBFX4" +}, +{ +"event":"token:ptok:v1:OGFRjEzuVhZrn1tlwWeQ5zNvk4_khFjSFKrMQY_YIME" +}, +{ +"event":"token=ptok:v1:OGFRjEzuVhZrn1tlwWeQ5zNvk4_khFjSFKrMQY_YIME" +}, +{ +"event":"token ptok:v1:_lXByvUrQgAiZ4EOeOXGT_PlbUd0M_GNzfg43nNBFX4" +}, +{ +"event":"token ptok:v1:OGFRjEzuVhZrn1tlwWeQ5zNvk4_khFjSFKrMQY_YIME" +}, +{ +"event":"token ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"message token=ptok:v1:7ZWOgZZFY3UYwHRVga6PD6VriwK0ettMsFZOMt0d2IY" +}, +{ +"event":"anyword_token ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"any_word_token ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"any-word_token ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"any-word-token ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"any_1-word_2-token ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"secret ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"any_1-word_2-secret ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"password ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"any_1-word_2-password ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"passwd ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"any_1-word_2-passwd ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"bearer ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"basic ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"digest ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"credential ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"credentials ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"authorization ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"authorization_header ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"authorization-header ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"authorisation ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"authorisation_header ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"authorisation-header ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"apikey ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"api_key ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"ptok:v1:BPI3sZpAYaaGwAQC5vdX9NK56OcYPacA_IRmfsKNz18" +}, +{ +"event":"secret-key ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"private-key ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"public-key ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"encryption-key ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"decryption-key ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"signing-key ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"access-key ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"master-key ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"root-key ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"session-key ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo" +}, +{ +"event":"secret_key=[SECRET-MASKED]" +}, +{ +"event":"'Authorization': 'ptok:v1:hG5NYyP8muNdI04cUjGKgpRa2ci25G--CJhvC722PJI'" +}, +{ +"event":"\"Authorization\": \"ptok:v1:hG5NYyP8muNdI04cUjGKgpRa2ci25G--CJhvC722PJI\"" +}, +{ +"event":"Authorization: ptok:v1:hG5NYyP8muNdI04cUjGKgpRa2ci25G--CJhvC722PJI" +}, +{ +"event":"Authorization= ptok:v1:hG5NYyP8muNdI04cUjGKgpRa2ci25G--CJhvC722PJI" +}, +{ +"event":"Authorization Bearer ptok:v1:OGFRjEzuVhZrn1tlwWeQ5zNvk4_khFjSFKrMQY_YIME" +}, +{ +"event":"{\"Authorization\": \"ptok:v1:_lXByvUrQgAiZ4EOeOXGT_PlbUd0M_GNzfg43nNBFX4\"}" +}, +{ +"event":"Here is {\"Authorization\": \"ptok:v1:_lXByvUrQgAiZ4EOeOXGT_PlbUd0M_GNzfg43nNBFX4\"}" +}, +{ +"event":"{\"Authorization\": \"ptok:v1:xwZelCJLrv6In2A4aVrz2O6_bPGyND5B0MDxmR0hmkM\"}" +}, +{ +"event":"Authorization: ptok:v1:Dp949MnXQasK4lMRYeS1Qn45Pljm-vbis-75-lgeiLw" +}, +{ +"event":"{\"Authorisation\": \"ptok:v1:Dp949MnXQasK4lMRYeS1Qn45Pljm-vbis-75-lgeiLw\"}" +}, +{ +"event":"Authentication failed with key 'ptok:v1:qDIRNe74Q6tSmwUe4fuytRREKT2HVfKEkFL8Z6h4egA'" +}, +{ +"event":"access_token=ptok:v1:TSsQJnxs4gq-u3wovFm24skGr5SOOxZJDuZcby0wbNk" +}, +{ +"event":"refresh_token=ptok:v1:TSsQJnxs4gq-u3wovFm24skGr5SOOxZJDuZcby0wbNk" +}, +{ +"event":"client_secret=ptok:v1:1Uc-bob3ywEPV_TX7RuoxshPDdrvBnjpYJK7cp-hYYY" +}, +{ +"event":"private_key=ptok:v1:TSsQJnxs4gq-u3wovFm24skGr5SOOxZJDuZcby0wbNk" +}, +{ +"event":"secret_key=ptok:v1:TSsQJnxs4gq-u3wovFm24skGr5SOOxZJDuZcby0wbNk" +}, +{ +"event":"basic= ptok:v1:_lXByvUrQgAiZ4EOeOXGT_PlbUd0M_GNzfg43nNBFX4" +}, +{ +"event":"ptok:v1:qDIRNe74Q6tSmwUe4fuytRREKT2HVfKEkFL8Z6h4egA" +}, +{ +"event":"Basic ptok:v1:dDYYfBksvPcnCNHNFCAelE8oUzi93GeVTmKUAF8nvqk" +}, +{ +"event":"body {\"public_key\": null, \"a\": 1}" +}, +{ +"event":"body {\"session_key\": true}" +}, +{ +"event":"body {'public_key': None}" +}, +{ +"event":"body {\"session_key\": \"ptok:v1:8O9tqpAd1nMMT3yelPozCFaxMX7IiICgfgF3iSXjz8k\"}" +}, +{ +"event":"body {'api_key': 'ptok:v1:8O9tqpAd1nMMT3yelPozCFaxMX7IiICgfgF3iSXjz8k'}" +}, +{ +"event":"{\"result\": \"SUCCESS\", \"session\": {\"aes256Key\": \"ptok:v1:kK2jZTAmp0_y-_RZE2TP3cz8F67sFwElkvPbQ4Hz4wI\", \"id\": \"SESS" +}, +{ +"event":"aes256Key=ptok:v1:kK2jZTAmp0_y-_RZE2TP3cz8F67sFwElkvPbQ4Hz4wI" +}, +{ +"event":"hmacSha256Key=ptok:v1:TSsQJnxs4gq-u3wovFm24skGr5SOOxZJDuZcby0wbNk" +}, +{ +"event":"privateKeyPem=ptok:v1:TSsQJnxs4gq-u3wovFm24skGr5SOOxZJDuZcby0wbNk" +}, +{ +"event":"codes_key=abcd1234efgh5678" +}, +{ +"event":"'cvv': '[CVV-MASKED]'" +}, +{ +"event":"'Cvv':'[CVV-MASKED]'" +}, +{ +"event":"\"cVv\": \"[CVV-MASKED]\"" +}, +{ +"event":"\"cvV\":\"[CVV-MASKED]\"" +}, +{ +"event":"CVv: [CVV-MASKED]" +}, +{ +"event":"cVV:[CVV-MASKED]" +}, +{ +"event":"CvV= [CVV-MASKED]" +}, +{ +"event":"CVV=[CVV-MASKED]" +}, +{ +"event":"Here cvv=[CVV-MASKED] is submitted" +}, +{ +"event":"'cvc': '[CVV-MASKED]'" +}, +{ +"event":"'CVC': '[CVV-MASKED]'" +}, +{ +"event":"{\"processed_data\": {\"cvv\": \"[CVV-MASKED]\"}}" +}, +{ +"event":"data {\"cvv\": \"[CVV-MASKED]\"}" +}, +{ +"event":"payment_id: ptok:v1:1bquJqVo3srNaRbuG70T9z6UbcmCgU5hKhrPyLm3VKU" +}, +{ +"event":"payment_id= ptok:v1:1bquJqVo3srNaRbuG70T9z6UbcmCgU5hKhrPyLm3VKU" +}, +{ +"event":"payment_id ptok:v1:1bquJqVo3srNaRbuG70T9z6UbcmCgU5hKhrPyLm3VKU" +}, +{ +"event":"payment-id: ptok:v1:1bquJqVo3srNaRbuG70T9z6UbcmCgU5hKhrPyLm3VKU" +}, +{ +"event":"payment-id= ptok:v1:1bquJqVo3srNaRbuG70T9z6UbcmCgU5hKhrPyLm3VKU" +}, +{ +"event":"payment-id ptok:v1:1bquJqVo3srNaRbuG70T9z6UbcmCgU5hKhrPyLm3VKU" +}, +{ +"event":"'payment_id': 'ptok:v1:1bquJqVo3srNaRbuG70T9z6UbcmCgU5hKhrPyLm3VKU'" +}, +{ +"event":"'payment-id': 'ptok:v1:1bquJqVo3srNaRbuG70T9z6UbcmCgU5hKhrPyLm3VKU'" +}, +{ +"event":"'payment_id':'ptok:v1:1bquJqVo3srNaRbuG70T9z6UbcmCgU5hKhrPyLm3VKU'" +}, +{ +"event":"'payment-id':'ptok:v1:1bquJqVo3srNaRbuG70T9z6UbcmCgU5hKhrPyLm3VKU'" +}, +{ +"event":"\"payment_id\": \"ptok:v1:1bquJqVo3srNaRbuG70T9z6UbcmCgU5hKhrPyLm3VKU\"" +}, +{ +"event":"\"payment-id\": \"ptok:v1:1bquJqVo3srNaRbuG70T9z6UbcmCgU5hKhrPyLm3VKU\"" +}, +{ +"event":"\"payment_id\":\"ptok:v1:1bquJqVo3srNaRbuG70T9z6UbcmCgU5hKhrPyLm3VKU\"" +}, +{ +"event":"\"payment-id\":\"ptok:v1:1bquJqVo3srNaRbuG70T9z6UbcmCgU5hKhrPyLm3VKU\"" +}, +{ +"event":"'transaction_id': 'ptok:v1:0VVBTqD-bT9JjNan0dbWD2IpFsPwKapzKfQ2ZPCNsQY'" +}, +{ +"event":"'auth_id': '[PAYMENT-ID-MASKED]'" +}, +{ +"event":"payment_id: [PAYMENT-ID-MASKED] done" +}, +{ +"event":"payment_id= [PAYMENT-ID-MASKED] done" +}, +{ +"event":"transaction_id: ptok:v1:XlNVNhxOUTgVaB1UzcCaMZknRPdAgZJ20RaCdg_JFGE" +}, +{ +"event":"transaction-id: ptok:v1:XlNVNhxOUTgVaB1UzcCaMZknRPdAgZJ20RaCdg_JFGE" +}, +{ +"event":"auth_id: ptok:v1:XlNVNhxOUTgVaB1UzcCaMZknRPdAgZJ20RaCdg_JFGE" +}, +{ +"event":"auth-id: ptok:v1:XlNVNhxOUTgVaB1UzcCaMZknRPdAgZJ20RaCdg_JFGE" +}, +{ +"event":"{\"processed_data\": {\"payment_id\": \"ptok:v1:1bquJqVo3srNaRbuG70T9z6UbcmCgU5hKhrPyLm3VKU\"}}" +}, +{ +"event":"account ptok:v1:4pnuHHw8Ta2XhDTSM67paaOyU_FIGzsIJaWpG-AkWyU credited" +}, +{ +"event":"acct ptok:v1:pzBGvacWoDg-ZFfX0K51qVdi2BEISC2z2bcP1yz2Zyk debited" +}, +{ +"event":"acct ptok:v1:i29WiKccWHrrpT2-R1b_I11Ug97tsacTifsMiYyah40 debited" +}, +{ +"event":"acct ptok:v1:g-SZfH2omZyVYk4fZr_ze0J58qaKBjqYoOhdWAJLx2w debited" +}, +{ +"event":"acct ptok:v1:TJiO60hhW6dcTZkgYBAQX3-_5rKw3N7lkNXhsNH7UQ0 debited" +}, +{ +"event":"acct ptok:v1:yN6qHKNiAnC9r6imfTWonwHxVCJ8RI_Zbg2ObyvnIvg debited" +}, +{ +"event":"acct ptok:v1:a8L8L5QzL3uYJLx7FPFaU9PMHx3Xi6-dTJGA8vijXlY debited" +}, +{ +"event":"acct ptok:v1:aqpspPshw9RbG7yhEB5L-Ad7eAAMK6hgfd-XuDLEdY0 debited" +}, +{ +"event":"acct ptok:v1:XS-9ZUMohgFodJylF0500Y8YkqySnpXidAEP7sdFk40 debited" +}, +{ +"event":"ptok:v1:gDlYdUIUkQZbmus1FJLlEYAUhdtLW3TlTUklf7eqj2c" +}, +{ +"event":"ptok:v1:gDlYdUIUkQZbmus1FJLlEYAUhdtLW3TlTUklf7eqj2c" +}, +{ +"event":"ptok:v1:gDlYdUIUkQZbmus1FJLlEYAUhdtLW3TlTUklf7eqj2c" +}, +{ +"event":"ptok:v1:gDlYdUIUkQZbmus1FJLlEYAUhdtLW3TlTUklf7eqj2c" +}, +{ +"event":"ptok:v1:lOiafQukK2ddrC6Gd_QK0g8aVvusxxigzv7LyZ1nRhA" +}, +{ +"event":"ptok:v1:lOiafQukK2ddrC6Gd_QK0g8aVvusxxigzv7LyZ1nRhA" +}, +{ +"event":"ptok:v1:lOiafQukK2ddrC6Gd_QK0g8aVvusxxigzv7LyZ1nRhA" +}, +{ +"event":"call ptok:v1:emM4ILoMX6tGxIQiyDns7y52AJRkhUmFm-Mu6BQp0bI now" +}, +{ +"event":"call ptok:v1:BFQxttUVRdBVP4IXaQcjudawbolAO_1Jsith6usx2ik now" +}, +{ +"event":"ptok:v1:rYi1lZa-Lxx9KQISmPti_1Xzvhivf27170HDufgjs6k" +}, +{ +"event":"contact ptok:v1:AoW38Cwh4OVIZRwWz5Lx2r81wisRN6haQhTnKkioXfU now" +}, +{ +"event":"ptok:v1:4xaL1xJcFwXzMA7EZEloqnMYKkePjN6CC9k4p9jmLHc" +}, +{ +"event":"ptok:v1:g2KSE-5SadRSm3fNuE_ZSwIkQYnNuMdFGiTgE7-KlEc" +}, +{ +"event":"ptok:v1:PeHcgjnNgkX5SwPisbS-3ugkutSN4sDeIk0j4PX5Qpk" +}, +{ +"event":"ptok:v1:s-XuwaGJfe2GEohzYhQufQ2gI57y-YCTg_ihqcSXA5k" +}, +{ +"event":"ptok:v1:0V6B6wiwdYZp7_wQtGNkFYmgqQE2ZLwZBTo19Grie14" +}, +{ +"event":"ptok:v1:KFfEaU4te4Kb3ZJ6QUZSYBDtvE_axEbGZnzseQbm778" +}, +{ +"event":"ptok:v1:LBFNre8vo3auF2ADj_XKMWVkUE0ls0ivCmdkSOmWmjQ" +}, +{ +"event":"notify: ptok:v1:EXeEqO-xvgQ0M4yCP07Hdqsjc-5V_Z5PJcSqnwaKTf8" +}, +{ +"event":"'email': 'ptok:v1:rYi1lZa-Lxx9KQISmPti_1Xzvhivf27170HDufgjs6k'" +}, +{ +"event":"\"email\": \"ptok:v1:rYi1lZa-Lxx9KQISmPti_1Xzvhivf27170HDufgjs6k\"" +}, +{ +"event":"email=ptok:v1:rYi1lZa-Lxx9KQISmPti_1Xzvhivf27170HDufgjs6k" +}, +{ +"event":"{\"contact\": {\"email\": \"ptok:v1:rYi1lZa-Lxx9KQISmPti_1Xzvhivf27170HDufgjs6k\"}}" +}, +{ +"event":"ptok:v1:_aWRbsN5VB9K61ABl7eN52wywzA1JU0kHB1w9CYLag8" +}, +{ +"event":"Here it's ptok:v1:_aWRbsN5VB9K61ABl7eN52wywzA1JU0kHB1w9CYLag8 failed!" +}, +{ +"event":"Here it's (ptok:v1:_aWRbsN5VB9K61ABl7eN52wywzA1JU0kHB1w9CYLag8) failed!" +}, +{ +"event":"Here it's: ptok:v1:_aWRbsN5VB9K61ABl7eN52wywzA1JU0kHB1w9CYLag8 failed!" +}, +{ +"event":"-ptok:v1:_aWRbsN5VB9K61ABl7eN52wywzA1JU0kHB1w9CYLag8" +}, +{ +"event":".ptok:v1:_aWRbsN5VB9K61ABl7eN52wywzA1JU0kHB1w9CYLag8" +}, +{ +"event":"ptok:v1:kRIP0Oc8sUQzmwjZ2ju_0DG7sPYsSbMplGr3IpkgdEA" +}, +{ +"event":"ptok:v1:QGj0Qfq3ti-UT28uqfOsofZyqMb3HscxUFds5M6-1hM" +}, +{ +"event":"ptok:v1:Z8KistoEjGGA6sr8Z9AldbgUYQQC_bEfUHTspNUByhU" +}, +{ +"event":"ptok:v1:at70G4JsvJ30mAKBl53VcNTUFi8FYK0W6puL5-mefNc" +}, +{ +"event":"token was ptok:v1:CBoJ8vE3I9u5CvS1CbI4JvXxtn4kSPZrLl7ToDtKjFo and more" +}, +{ +"event":"********8912" +}, +{ +"event":"*********9123" +}, +{ +"event":"**********1234" +}, +{ +"event":"912345*****2345" +}, +{ +"event":"912345******3456" +}, +{ +"event":"912345*******4567" +}, +{ +"event":"912345********5678" +}, +{ +"event":"912345*********6789" +}, +{ +"event":"********8912" +}, +{ +"event":"*********9123" +}, +{ +"event":"**********1234" +}, +{ +"event":"912345*****2345" +}, +{ +"event":"912345******3456" +}, +{ +"event":"912345*******4567" +}, +{ +"event":"912345********5678" +}, +{ +"event":"912345*********6789" +}, +{ +"event":"********8912" +}, +{ +"event":"*********9123" +}, +{ +"event":"**********1234" +}, +{ +"event":"912345*****2345" +}, +{ +"event":"912345******3456" +}, +{ +"event":"912345*******4567" +}, +{ +"event":"912345********5678" +}, +{ +"event":"912345*********6789" +}, +{ +"event":"********8912" +}, +{ +"event":"*********9123" +}, +{ +"event":"**********1234" +}, +{ +"event":"112345*****2345" +}, +{ +"event":"112345******3456" +}, +{ +"event":"112345*******4567" +}, +{ +"event":"112345********5678" +} +], +"keyset/every-pack/prose":[ +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +"{\"session_id\": \"21a2496f8b6c193ec61b8dd52cbe8d827b4eaa49\", \"order_no\": \"ORD-2026-000123\", \"amount\": \"12.500\", \"currency_code\": \"KWD\", \"customer_email\": \"ptok:v1:eMwFkFHAafEM2yUlbFIkhBKSUhHqDcNpxE3bLyWFI54\", \"customer_phone\": \"ptok:v1:kVcmxuM7xSNaFxT-GvmeIwsb_FQMPsR72OBNUiZEAQc\", \"customer_first_name\": \"ptok:v1:NVSAuFYU9wWRKK_cQt9wbZTw0HOXuh5NgHiMxgv64o8\", \"billing_address\": {\"line1\": \"ptok:v1:Ts0ReoBNKiJBYHXTW1WbXX7iUnDW6VwUwCUjR_LLNNY\", \"city\": \"ptok:v1:fKrE4cXxHekZk4mklS_6gF9X89UW19yWC8H9AaftrcE\", \"country\": \"ptok:v1:9hw5Sp-i6dZADYPdeIHwEOV3awFcLyE-i2AgY5JGTGs\"}, \"pg_codes\": [\"mpgs-direct\", \"cs-direct\", \"kpay\"], \"redirect_url\": \"https://merchant.example/return?ref=abc123&lang=en\", \"webhook_url\": \"https://merchant.example/hooks/ottu\", \"extra\": {\"note\": \"gift wrap\", \"items\": [{\"sku\": \"SKU-0\", \"qty\": 0}, {\"sku\": \"SKU-1\", \"qty\": 1}, {\"sku\": \"SKU-2\", \"qty\": 2}, {\"sku\": \"SKU-3\", \"qty\": 3}, {\"sku\": \"SKU-4\", \"qty\": 4}, {\"sku\": \"SKU-5\", \"qty\": 5}, {\"sku\": \"SKU-6\", \"qty\": 6}, {\"sku\": \"SKU-7\", \"qty\": 7}, {\"sku\": \"SKU-8\", \"qty\": 8}, {\"sku\": \"SKU-9\", \"qty\": 9}]}, \"card_acceptance_criteria\": {\"min_expiry_time\": 30}, \"three_ds\": {\"directory_server_transaction_id\": \"ptok:v1:LXiC7pzkZj27hoZISoe9Fas88oeB2ULx3PiqcFJv3v8\"}}", +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null +], +"keyset/every-pack/records":[ +[ +"user %s paid %s KWD for %s", +"('bob', '12.500', 'ORD-1')", +"user bob paid 12.500 KWD for ORD-1" +], +[ +"cvv=[CVV-MASKED]", +"()", +"cvv=[CVV-MASKED]" +], +[ +"card 411111******1111 [CVV-MASKED]", +"()", +"card 411111******1111 [CVV-MASKED]" +], +[ +"password=ptok:v1:YAlHtTNhC_kDTbpjnErrOlkwieoPRffgVWW1cGq95zk", +"()", +"password=ptok:v1:YAlHtTNhC_kDTbpjnErrOlkwieoPRffgVWW1cGq95zk" +], +[ +"paid 10 KWD by ptok:v1:8NokGXtoHzu2rwxmzOBYr1mRB7NV7_DRf2UFGjENzpM", +"()", +"paid 10 KWD by ptok:v1:8NokGXtoHzu2rwxmzOBYr1mRB7NV7_DRf2UFGjENzpM" +], +[ +"sent to %s", +"('ptok:v1:q-aBDA-3PLgNB0wyeH6tCsUcbaLAm6sTRaHYrmp7nGI',)", +"sent to ptok:v1:q-aBDA-3PLgNB0wyeH6tCsUcbaLAm6sTRaHYrmp7nGI" +], +[ +"apple pay token %s", +"('[SAD-MASKED]',)", +"apple pay token [SAD-MASKED]" +], +[ +"retried with Bearer ptok:v1:iWjJ0BTFxqptSpf9yTbBc2WW0qn3L3x0WxlYMRSohJI", +"()", +"retried with Bearer ptok:v1:iWjJ0BTFxqptSpf9yTbBc2WW0qn3L3x0WxlYMRSohJI" +], +[ +"%s", +"{'card_number': '411111******1111', 'cvv': '[CVV-MASKED]'}", +"{'card_number': '411111******1111', 'cvv': '[CVV-MASKED]'}" +] +], +"keyset/every-pack/text":[ +"ptok:v1:BCng-t9pGDNJZb8Kxr-8dbnx0vH9S1VGixXZwTjuAXw", +"ptok:v1:fbBTZVC9H_ZhvwccVlIcAT81aJ11pahAQwMADLNidjk", +"ptok:v1:KQvnEFp44z39aJFKt8AgvxsnNi558q_g7PpLLFmjhU8", +"ptok:v1:IPPblK6aMP_OH1mPa_NmJEku5a8A4DNuj0e8_FswPm0", +"ptok:v1:nMYkVx-T7gC81k-ugXTnuEVARP23rbgqh68K2StP3Fw", +"'token': 'ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo'", +"'token':'ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo'", +"\"token\": \"ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo\"", +"\"token\":\"ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo\"", +"token: ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"token:ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"token= ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"token=ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"token=ptok:v1:7ZWOgZZFY3UYwHRVga6PD6VriwK0ettMsFZOMt0d2IY", +"token=ptok:v1:JuuCkMnRlUJr5bdNk7w-vsvpb9mqq7UBGofK_xBj-0I", +"token: ptok:v1:7ZWOgZZFY3UYwHRVga6PD6VriwK0ettMsFZOMt0d2IY", +"token: ptok:v1:JuuCkMnRlUJr5bdNk7w-vsvpb9mqq7UBGofK_xBj-0I", +"token:ptok:v1:_lXByvUrQgAiZ4EOeOXGT_PlbUd0M_GNzfg43nNBFX4", +"token=ptok:v1:_lXByvUrQgAiZ4EOeOXGT_PlbUd0M_GNzfg43nNBFX4", +"token:ptok:v1:OGFRjEzuVhZrn1tlwWeQ5zNvk4_khFjSFKrMQY_YIME", +"token=ptok:v1:OGFRjEzuVhZrn1tlwWeQ5zNvk4_khFjSFKrMQY_YIME", +"token ptok:v1:_lXByvUrQgAiZ4EOeOXGT_PlbUd0M_GNzfg43nNBFX4", +"token ptok:v1:OGFRjEzuVhZrn1tlwWeQ5zNvk4_khFjSFKrMQY_YIME", +"token ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"message token=ptok:v1:7ZWOgZZFY3UYwHRVga6PD6VriwK0ettMsFZOMt0d2IY", +"anyword_token ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"any_word_token ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"any-word_token ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"any-word-token ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"any_1-word_2-token ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"secret ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"any_1-word_2-secret ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"password ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"any_1-word_2-password ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"passwd ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"any_1-word_2-passwd ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"bearer ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"basic ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"digest ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"credential ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"credentials ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"authorization ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"authorization_header ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"authorization-header ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"authorisation ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"authorisation_header ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"authorisation-header ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"apikey ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"api_key ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"ptok:v1:BPI3sZpAYaaGwAQC5vdX9NK56OcYPacA_IRmfsKNz18", +"secret-key ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"private-key ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"public-key ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"encryption-key ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"decryption-key ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"signing-key ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"access-key ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"master-key ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"root-key ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"session-key ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"secret_key=[SECRET-MASKED]", +"'Authorization': 'ptok:v1:hG5NYyP8muNdI04cUjGKgpRa2ci25G--CJhvC722PJI'", +"\"Authorization\": \"ptok:v1:hG5NYyP8muNdI04cUjGKgpRa2ci25G--CJhvC722PJI\"", +"Authorization: ptok:v1:hG5NYyP8muNdI04cUjGKgpRa2ci25G--CJhvC722PJI", +"Authorization= ptok:v1:hG5NYyP8muNdI04cUjGKgpRa2ci25G--CJhvC722PJI", +"Authorization Bearer ptok:v1:OGFRjEzuVhZrn1tlwWeQ5zNvk4_khFjSFKrMQY_YIME", +"{\"Authorization\": \"ptok:v1:_lXByvUrQgAiZ4EOeOXGT_PlbUd0M_GNzfg43nNBFX4\"}", +"Here is {\"Authorization\": \"ptok:v1:_lXByvUrQgAiZ4EOeOXGT_PlbUd0M_GNzfg43nNBFX4\"}", +"{\"Authorization\": \"ptok:v1:xwZelCJLrv6In2A4aVrz2O6_bPGyND5B0MDxmR0hmkM\"}", +"Authorization: ptok:v1:Dp949MnXQasK4lMRYeS1Qn45Pljm-vbis-75-lgeiLw", +"{\"Authorisation\": \"ptok:v1:Dp949MnXQasK4lMRYeS1Qn45Pljm-vbis-75-lgeiLw\"}", +"Authentication failed with key 'ptok:v1:qDIRNe74Q6tSmwUe4fuytRREKT2HVfKEkFL8Z6h4egA'", +"access_token=ptok:v1:TSsQJnxs4gq-u3wovFm24skGr5SOOxZJDuZcby0wbNk", +"refresh_token=ptok:v1:TSsQJnxs4gq-u3wovFm24skGr5SOOxZJDuZcby0wbNk", +"client_secret=ptok:v1:1Uc-bob3ywEPV_TX7RuoxshPDdrvBnjpYJK7cp-hYYY", +"private_key=ptok:v1:TSsQJnxs4gq-u3wovFm24skGr5SOOxZJDuZcby0wbNk", +"secret_key=ptok:v1:TSsQJnxs4gq-u3wovFm24skGr5SOOxZJDuZcby0wbNk", +"basic= ptok:v1:_lXByvUrQgAiZ4EOeOXGT_PlbUd0M_GNzfg43nNBFX4", +"ptok:v1:qDIRNe74Q6tSmwUe4fuytRREKT2HVfKEkFL8Z6h4egA", +"Basic ptok:v1:dDYYfBksvPcnCNHNFCAelE8oUzi93GeVTmKUAF8nvqk", +null, +null, +null, +"body {\"session_key\": \"ptok:v1:8O9tqpAd1nMMT3yelPozCFaxMX7IiICgfgF3iSXjz8k\"}", +"body {'api_key': 'ptok:v1:8O9tqpAd1nMMT3yelPozCFaxMX7IiICgfgF3iSXjz8k'}", +"{\"result\": \"SUCCESS\", \"session\": {\"aes256Key\": \"ptok:v1:kK2jZTAmp0_y-_RZE2TP3cz8F67sFwElkvPbQ4Hz4wI\", \"id\": \"SESS", +"aes256Key=ptok:v1:kK2jZTAmp0_y-_RZE2TP3cz8F67sFwElkvPbQ4Hz4wI", +"hmacSha256Key=ptok:v1:TSsQJnxs4gq-u3wovFm24skGr5SOOxZJDuZcby0wbNk", +"privateKeyPem=ptok:v1:TSsQJnxs4gq-u3wovFm24skGr5SOOxZJDuZcby0wbNk", +null, +"'cvv': '[CVV-MASKED]'", +"'Cvv':'[CVV-MASKED]'", +"\"cVv\": \"[CVV-MASKED]\"", +"\"cvV\":\"[CVV-MASKED]\"", +"CVv: [CVV-MASKED]", +"cVV:[CVV-MASKED]", +"CvV= [CVV-MASKED]", +"CVV=[CVV-MASKED]", +"Here cvv=[CVV-MASKED] is submitted", +"'cvc': '[CVV-MASKED]'", +"'CVC': '[CVV-MASKED]'", +"{\"processed_data\": {\"cvv\": \"[CVV-MASKED]\"}}", +"data {\"cvv\": \"[CVV-MASKED]\"}", +"payment_id: ptok:v1:1bquJqVo3srNaRbuG70T9z6UbcmCgU5hKhrPyLm3VKU", +"payment_id= ptok:v1:1bquJqVo3srNaRbuG70T9z6UbcmCgU5hKhrPyLm3VKU", +"payment_id ptok:v1:1bquJqVo3srNaRbuG70T9z6UbcmCgU5hKhrPyLm3VKU", +"payment-id: ptok:v1:1bquJqVo3srNaRbuG70T9z6UbcmCgU5hKhrPyLm3VKU", +"payment-id= ptok:v1:1bquJqVo3srNaRbuG70T9z6UbcmCgU5hKhrPyLm3VKU", +"payment-id ptok:v1:1bquJqVo3srNaRbuG70T9z6UbcmCgU5hKhrPyLm3VKU", +"'payment_id': 'ptok:v1:1bquJqVo3srNaRbuG70T9z6UbcmCgU5hKhrPyLm3VKU'", +"'payment-id': 'ptok:v1:1bquJqVo3srNaRbuG70T9z6UbcmCgU5hKhrPyLm3VKU'", +"'payment_id':'ptok:v1:1bquJqVo3srNaRbuG70T9z6UbcmCgU5hKhrPyLm3VKU'", +"'payment-id':'ptok:v1:1bquJqVo3srNaRbuG70T9z6UbcmCgU5hKhrPyLm3VKU'", +"\"payment_id\": \"ptok:v1:1bquJqVo3srNaRbuG70T9z6UbcmCgU5hKhrPyLm3VKU\"", +"\"payment-id\": \"ptok:v1:1bquJqVo3srNaRbuG70T9z6UbcmCgU5hKhrPyLm3VKU\"", +"\"payment_id\":\"ptok:v1:1bquJqVo3srNaRbuG70T9z6UbcmCgU5hKhrPyLm3VKU\"", +"\"payment-id\":\"ptok:v1:1bquJqVo3srNaRbuG70T9z6UbcmCgU5hKhrPyLm3VKU\"", +"'transaction_id': 'ptok:v1:0VVBTqD-bT9JjNan0dbWD2IpFsPwKapzKfQ2ZPCNsQY'", +"'auth_id': '[PAYMENT-ID-MASKED]'", +"payment_id: [PAYMENT-ID-MASKED] done", +"payment_id= [PAYMENT-ID-MASKED] done", +"transaction_id: ptok:v1:XlNVNhxOUTgVaB1UzcCaMZknRPdAgZJ20RaCdg_JFGE", +"transaction-id: ptok:v1:XlNVNhxOUTgVaB1UzcCaMZknRPdAgZJ20RaCdg_JFGE", +"auth_id: ptok:v1:XlNVNhxOUTgVaB1UzcCaMZknRPdAgZJ20RaCdg_JFGE", +"auth-id: ptok:v1:XlNVNhxOUTgVaB1UzcCaMZknRPdAgZJ20RaCdg_JFGE", +"{\"processed_data\": {\"payment_id\": \"ptok:v1:1bquJqVo3srNaRbuG70T9z6UbcmCgU5hKhrPyLm3VKU\"}}", +"account ptok:v1:4pnuHHw8Ta2XhDTSM67paaOyU_FIGzsIJaWpG-AkWyU credited", +"acct ptok:v1:pzBGvacWoDg-ZFfX0K51qVdi2BEISC2z2bcP1yz2Zyk debited", +"acct ptok:v1:i29WiKccWHrrpT2-R1b_I11Ug97tsacTifsMiYyah40 debited", +"acct ptok:v1:g-SZfH2omZyVYk4fZr_ze0J58qaKBjqYoOhdWAJLx2w debited", +"acct ptok:v1:TJiO60hhW6dcTZkgYBAQX3-_5rKw3N7lkNXhsNH7UQ0 debited", +"acct ptok:v1:yN6qHKNiAnC9r6imfTWonwHxVCJ8RI_Zbg2ObyvnIvg debited", +"acct ptok:v1:a8L8L5QzL3uYJLx7FPFaU9PMHx3Xi6-dTJGA8vijXlY debited", +"acct ptok:v1:aqpspPshw9RbG7yhEB5L-Ad7eAAMK6hgfd-XuDLEdY0 debited", +"acct ptok:v1:XS-9ZUMohgFodJylF0500Y8YkqySnpXidAEP7sdFk40 debited", +"ptok:v1:gDlYdUIUkQZbmus1FJLlEYAUhdtLW3TlTUklf7eqj2c", +"ptok:v1:gDlYdUIUkQZbmus1FJLlEYAUhdtLW3TlTUklf7eqj2c", +"ptok:v1:gDlYdUIUkQZbmus1FJLlEYAUhdtLW3TlTUklf7eqj2c", +"ptok:v1:gDlYdUIUkQZbmus1FJLlEYAUhdtLW3TlTUklf7eqj2c", +"ptok:v1:lOiafQukK2ddrC6Gd_QK0g8aVvusxxigzv7LyZ1nRhA", +"ptok:v1:lOiafQukK2ddrC6Gd_QK0g8aVvusxxigzv7LyZ1nRhA", +"ptok:v1:lOiafQukK2ddrC6Gd_QK0g8aVvusxxigzv7LyZ1nRhA", +"call ptok:v1:emM4ILoMX6tGxIQiyDns7y52AJRkhUmFm-Mu6BQp0bI now", +"call ptok:v1:BFQxttUVRdBVP4IXaQcjudawbolAO_1Jsith6usx2ik now", +"ptok:v1:rYi1lZa-Lxx9KQISmPti_1Xzvhivf27170HDufgjs6k", +"contact ptok:v1:AoW38Cwh4OVIZRwWz5Lx2r81wisRN6haQhTnKkioXfU now", +"ptok:v1:4xaL1xJcFwXzMA7EZEloqnMYKkePjN6CC9k4p9jmLHc", +"ptok:v1:g2KSE-5SadRSm3fNuE_ZSwIkQYnNuMdFGiTgE7-KlEc", +"ptok:v1:PeHcgjnNgkX5SwPisbS-3ugkutSN4sDeIk0j4PX5Qpk", +"ptok:v1:s-XuwaGJfe2GEohzYhQufQ2gI57y-YCTg_ihqcSXA5k", +"ptok:v1:0V6B6wiwdYZp7_wQtGNkFYmgqQE2ZLwZBTo19Grie14", +"ptok:v1:KFfEaU4te4Kb3ZJ6QUZSYBDtvE_axEbGZnzseQbm778", +"ptok:v1:LBFNre8vo3auF2ADj_XKMWVkUE0ls0ivCmdkSOmWmjQ", +"notify: ptok:v1:EXeEqO-xvgQ0M4yCP07Hdqsjc-5V_Z5PJcSqnwaKTf8", +"'email': 'ptok:v1:rYi1lZa-Lxx9KQISmPti_1Xzvhivf27170HDufgjs6k'", +"\"email\": \"ptok:v1:rYi1lZa-Lxx9KQISmPti_1Xzvhivf27170HDufgjs6k\"", +"email=ptok:v1:rYi1lZa-Lxx9KQISmPti_1Xzvhivf27170HDufgjs6k", +"{\"contact\": {\"email\": \"ptok:v1:rYi1lZa-Lxx9KQISmPti_1Xzvhivf27170HDufgjs6k\"}}", +"ptok:v1:_aWRbsN5VB9K61ABl7eN52wywzA1JU0kHB1w9CYLag8", +"Here it's ptok:v1:_aWRbsN5VB9K61ABl7eN52wywzA1JU0kHB1w9CYLag8 failed!", +"Here it's (ptok:v1:_aWRbsN5VB9K61ABl7eN52wywzA1JU0kHB1w9CYLag8) failed!", +"Here it's: ptok:v1:_aWRbsN5VB9K61ABl7eN52wywzA1JU0kHB1w9CYLag8 failed!", +"-ptok:v1:_aWRbsN5VB9K61ABl7eN52wywzA1JU0kHB1w9CYLag8", +".ptok:v1:_aWRbsN5VB9K61ABl7eN52wywzA1JU0kHB1w9CYLag8", +"ptok:v1:kRIP0Oc8sUQzmwjZ2ju_0DG7sPYsSbMplGr3IpkgdEA", +"ptok:v1:QGj0Qfq3ti-UT28uqfOsofZyqMb3HscxUFds5M6-1hM", +"ptok:v1:Z8KistoEjGGA6sr8Z9AldbgUYQQC_bEfUHTspNUByhU", +"ptok:v1:at70G4JsvJ30mAKBl53VcNTUFi8FYK0W6puL5-mefNc", +"token was ptok:v1:CBoJ8vE3I9u5CvS1CbI4JvXxtn4kSPZrLl7ToDtKjFo and more", +"********8912", +"*********9123", +"**********1234", +"912345*****2345", +"912345******3456", +"912345*******4567", +"912345********5678", +"912345*********6789", +"********8912", +"*********9123", +"**********1234", +"912345*****2345", +"912345******3456", +"912345*******4567", +"912345********5678", +"912345*********6789", +"********8912", +"*********9123", +"**********1234", +"912345*****2345", +"912345******3456", +"912345*******4567", +"912345********5678", +"912345*********6789", +"********8912", +"*********9123", +"**********1234", +"112345*****2345", +"112345******3456", +"112345*******4567", +"112345********5678", +"112345*********6789", +"********8912", +"*********9123", +"**********1234", +"112345*****2345", +"112345******3456", +"112345*******4567", +"112345********5678", +"112345*********6789", +"********8912", +"*********9123", +"**********1234", +"112345*****2345", +"112345******3456", +"112345*******4567", +"112345********5678", +"112345*********6789", +"903456******1256", +"903456******1256", +"103456******1256", +"103456******1256", +"903456*********0345", +"903456*********0345", +"103456*********0345", +"103456*********0345", +"903456*********6789", +"903456*********6789", +"403456*********6789", +"403456*********6789", +"********1290", +"903456*****0125", +"903456*********6125", +"********1290", +"403456*****0125", +"403456*********6125", +"Here is 912345*********6789 card Number", +"Here is (912345*********6789) card Number", +"Here is [912345*********6789] card Number", +"Here is {912345*********6789} card Number", +"Here num:912345*********6789 card Number", +"Here num=912345*********6789 card Number", +"Here num,912345*********6789 card Number", +"Here num.912345*********6789 card Number", +"********8912", +"112345*****2345", +"112345******3456", +"112345*******4567", +"112345*********6789", +"point 1 912345*********6789", +"point 1 123456*********7891", +"qty 2 411111******1111", +"qty 2 912345*********6789", +"10:00:004111********1111", +"10:00:00 912345*********6789", +"912345**********7891", +"912345**********7891", +"112345 **********7891", +"1123-4567-********7891", +"912345**********7891", +"1123 4567 ********7891", +"ssn ptok:v1:7yztZX2K_RJ5YNFazxRSSBP59fHRGiLB7ZmeZ7QAZ4Y on file", +"ssn ptok:v1:7yztZX2K_RJ5YNFazxRSSBP59fHRGiLB7ZmeZ7QAZ4Y on file", +"ssn ptok:v1:7yztZX2K_RJ5YNFazxRSSBP59fHRGiLB7ZmeZ7QAZ4Y on file", +"ssn ptok:v1:7yztZX2K_RJ5YNFazxRSSBP59fHRGiLB7ZmeZ7QAZ4Y on file", +"ssn ptok:v1:7yztZX2K_RJ5YNFazxRSSBP59fHRGiLB7ZmeZ7QAZ4Y on file", +"ssn ptok:v1:7yztZX2K_RJ5YNFazxRSSBP59fHRGiLB7ZmeZ7QAZ4Y on file", +"ssn ptok:v1:7yztZX2K_RJ5YNFazxRSSBP59fHRGiLB7ZmeZ7QAZ4Y on file", +"ssn ptok:v1:7yztZX2K_RJ5YNFazxRSSBP59fHRGiLB7ZmeZ7QAZ4Y on file", +"ssn ptok:v1:7yztZX2K_RJ5YNFazxRSSBP59fHRGiLB7ZmeZ7QAZ4Y on file", +"ssn=ptok:v1:7yztZX2K_RJ5YNFazxRSSBP59fHRGiLB7ZmeZ7QAZ4Y on file", +"ssn:ptok:v1:7yztZX2K_RJ5YNFazxRSSBP59fHRGiLB7ZmeZ7QAZ4Y on file", +"ssn,ptok:v1:7yztZX2K_RJ5YNFazxRSSBP59fHRGiLB7ZmeZ7QAZ4Y on file", +"ssn.ptok:v1:7yztZX2K_RJ5YNFazxRSSBP59fHRGiLB7ZmeZ7QAZ4Y on file", +"ssn'ptok:v1:7yztZX2K_RJ5YNFazxRSSBP59fHRGiLB7ZmeZ7QAZ4Y' on file", +"ssn\"ptok:v1:7yztZX2K_RJ5YNFazxRSSBP59fHRGiLB7ZmeZ7QAZ4Y\" on file", +"ssn(ptok:v1:7yztZX2K_RJ5YNFazxRSSBP59fHRGiLB7ZmeZ7QAZ4Y) on file", +"ssn[ptok:v1:7yztZX2K_RJ5YNFazxRSSBP59fHRGiLB7ZmeZ7QAZ4Y] on file", +"ssn{ptok:v1:7yztZX2K_RJ5YNFazxRSSBP59fHRGiLB7ZmeZ7QAZ4Y} on file", +"ptok:v1:7yztZX2K_RJ5YNFazxRSSBP59fHRGiLB7ZmeZ7QAZ4Y on file", +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +"", +"error: client_secret=\"[SECRET-MASKED]\" rejected", +"", +"x", +"{\"password\": \"[SECRET-MASKED]}", +"{\"id\": 1, \"password\": \"[SECRET-MASKED] x", +"password=[SECRET-MASKED], status=ok", +"password=[SECRET-MASKED];", +"password=[SECRET-MASKED]'", +"password=ptok:v1:dr-HgNJ4UDYqKoC3DRkGk0Omu6F_noyGV_qv7IPOkck\",411111******1111&x=1", +"password=[SECRET-MASKED]\":cd&x=1", +"password=\"[SECRET-MASKED]\"&x=1", +"password=[SECRET-MASKED]&x=1", +"{\"xml\": \"\"}", +"{\"note\": \"password=\\\"[SECRET-MASKED]\\\"&x=1\"}", +"{\"note\": \"password=[SECRET-MASKED]&x=1\"}", +"{\"password\": \"[SECRET-MASKED]\"}", +"{\"url\": \"https://x?secret=[SECRET-MASKED]\"}", +"{\"password\": \"[SECRET-MASKED]\"}", +"could not connect postgresql://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@localhost:5432/db", +"amqp://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@10.0.0.5//", +"GET https://ptok:v1:XNqMZh8_iZ4x3AH-pJfWEYF2XEjwSE8Jy6Hpw0kzvfU:@api.host/ failed", +"redis://:ptok:v1:TWnXyioKUufAQBQI4vDrZz31kmYDMo7i361DTzsYvVQ@cache:6379/0 refused", +"{\"dsn\": \"mongodb+srv://ptok:v1:3WnZ59Xqf2mMLZBvWq7gO76gJtmvr_nfOIF6R72WfJQ:ptok:v1:4EFf80ox7WcRxu_z-w_VnRFg9KnlNoO979cmu96PuxQ@cluster0.example.net/app\"}", +"fetch https://ptok:v1:6n-pFEJVVwxWmu2_xyBdOUs1aJOAJP177HYqFGRgX7c:ptok:v1:6v1czs-cr9ln5giu3wHsk9fPNC-TOXDbgiu5dhICHoA@api.host.example/v1/pay", +"amqp://ptok:v1:4kf5kuCKD3JuIoIlyxnv-CnLf3MgtB-toB95NX0xM0w:ptok:v1:EOdvSQI94XP5F1uDI01KpMTg5Go2zoWZuCYkVOCRlPY@mq:5672/", +"amqp://ptok:v1:Jzk0v_fK13PuB3KgethJ2JmmdjZjm06L6vYscLDVwdA:ptok:v1:UpWqd45iNzs8JfIp2AfeTBreV2LXkfZK_NyUoo_qch4@mq:5672/", +"sent {\"password\": \"ptok:v1:ACviYdsWVCvlWaJzzKIvSwsM3M5gbAOH6xO6aPy_c7I\"} to the gateway", +"login with password=ptok:v1:ACviYdsWVCvlWaJzzKIvSwsM3M5gbAOH6xO6aPy_c7I failed", +"retried with Bearer ptok:v1:ACviYdsWVCvlWaJzzKIvSwsM3M5gbAOH6xO6aPy_c7I", +"411111******1111 [CVV-MASKED]", +"411111******1111 [CVV-MASKED]", +"411111******1111|12/27|[CVV-MASKED]", +"pan 411111******1111 12/27 [CVV-MASKED] ok", +"411111******1111,12,27,[CVV-MASKED]", +"411111******1111;[CVV-MASKED];[CVV-MASKED]", +"411111******1111 [CVV-MASKED] [CVV-MASKED]", +"pan 411111******1111 [CVV-MASKED]", +"card 411111******1111 exp 12/27 [CVV-MASKED]", +"card 411111******1111 expiry: 1227 [CVV-MASKED]", +"411111******1111 [CVV-MASKED] 12/27", +"411111******1111\n[CVV-MASKED]", +"paid with 411111******1111 [CVV-MASKED].", +"411111******1111: [CVV-MASKED]", +"********0002 [CVV-MASKED]", +"********1111 [CVV-MASKED]", +"REF4111111111111111 [CVV-MASKED]", +"the cvv is [CVV-MASKED]", +"security code was [CVV-MASKED]", +"cvv - [CVV-MASKED] entered", +"CVV2 # [CVV-MASKED]", +"cvv number [CVV-MASKED]", +"card 411111******1111 cvv [CVV-MASKED]", +"cvv=[CVV-MASKED] for 411111******1111", +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +"TRANPORTAL123ptok:v1:AQ6W6lJiUPhhfswZOx3SLOQ5DZ_fAoBspUjCrPV_4lA110.000TRK1{\"paymentData\":\"[SAD-MASKED]\"}", +"TRANPORTAL123ptok:v1:AQ6W6lJiUPhhfswZOx3SLOQ5DZ_fAoBspUjCrPV_4lA810.000202612345TRK1TrackID", +"TRANPORTAL123ptok:v1:AQ6W6lJiUPhhfswZOx3SLOQ5DZ_fAoBspUjCrPV_4lA25.000202612345TRK1TrackID", +"{\"session_id\": \"21a2496f8b6c193ec61b8dd52cbe8d827b4eaa49\", \"order_no\": \"ORD-2026-000123\", \"amount\": \"12.500\", \"currency_code\": \"KWD\", \"customer_email\": \"ptok:v1:eMwFkFHAafEM2yUlbFIkhBKSUhHqDcNpxE3bLyWFI54\", \"customer_phone\": \"ptok:v1:kVcmxuM7xSNaFxT-GvmeIwsb_FQMPsR72OBNUiZEAQc\", \"customer_first_name\": \"Jane\", \"billing_address\": {\"line1\": \"Block 1, Street 2\", \"city\": \"Kuwait City\", \"country\": \"KW\"}, \"pg_codes\": [\"mpgs-direct\", \"cs-direct\", \"kpay\"], \"redirect_url\": \"https://merchant.example/return?ref=abc123&lang=en\", \"webhook_url\": \"https://merchant.example/hooks/ottu\", \"extra\": {\"note\": \"gift wrap\", \"items\": [{\"sku\": \"SKU-0\", \"qty\": 0}, {\"sku\": \"SKU-1\", \"qty\": 1}, {\"sku\": \"SKU-2\", \"qty\": 2}, {\"sku\": \"SKU-3\", \"qty\": 3}, {\"sku\": \"SKU-4\", \"qty\": 4}, {\"sku\": \"SKU-5\", \"qty\": 5}, {\"sku\": \"SKU-6\", \"qty\": 6}, {\"sku\": \"SKU-7\", \"qty\": 7}, {\"sku\": \"SKU-8\", \"qty\": 8}, {\"sku\": \"SKU-9\", \"qty\": 9}]}, \"card_acceptance_criteria\": {\"min_expiry_time\": 30}, \"three_ds\": {\"directory_server_transaction_id\": \"3d85c5d3-8412-4f1d-9e26-4a0f00123456\"}}", +null, +"[SAD-MASKED]ptok:v1:q-aBDA-3PLgNB0wyeH6tCsUcbaLAm6sTRaHYrmp7nGI]", +"https://[SECRET-MASKED]@h.example/", +"[the cvv is ptok:v1:kVcmxuM7xSNaFxT-GvmeIwsb_FQMPsR72OBNUiZEAQc", +"********9350 mshttps://[SECRET-MASKED]@h.example/[SAD-MASKED],", +"Bearer [SECRET-MASKED]\": \"", +null, +"Authorization: ptok:v1:4EFf80ox7WcRxu_z-w_VnRFg9KnlNoO979cmu96PuxQ[)", +null, +"12/27abc123\"{{,ptok:v1:UoGOZOZipLeBOQc4K2s1IUU09YK1XVLK7WXRdS7HO-o cvv is )", +"(ptok:v1:uWivpUz4ccwadzczzDqfbKX8Hc8q0iHea90LngYZxRY+96550000000token=ptok:v1:q-aBDA-3PLgNB0wyeH6tCsUcbaLAm6sTRaHYrmp7nGI,{\"[SAD-MASKED]\"12/27", +"}cvv https://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@h/"", +null, +"https://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@h/abc123cvv=:cvv |", +null, +"ptok:v1:q-aBDA-3PLgNB0wyeH6tCsUcbaLAm6sTRaHYrmp7nGI\\", +null, +"350 ms\\token=ptok:v1:fUFDP3wpQT6UVBtFrk6CbAfbhwq1z-qOuGJea1ro8Ls}password=}{Authorization: \"ptok:v1:r2HMq_CFEIsYZySf_MlUxRN1WoQRk0vAmUnzLSPnyDA\": \"", +"1234Bearer \"https://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@h/[SAD-MASKED]\"token\": \"", +"[CVV-MASKED](", +"200\"[SAD-MASKED]\"12/27)jane@example.com123411111111111111112/27200", +null, +null, +"123[cvv=,ptok:v1:kVcmxuM7xSNaFxT-GvmeIwsb_FQMPsR72OBNUiZEAQc", +",(\"[SAD-MASKED]\"Authorization: ptok:v1:1K9jSW00oRl0vDLwnRWeiTcOCahAMdlJcmWGIvX62Uc>", +"ptok:v1:4pnuHHw8Ta2XhDTSM67paaOyU_FIGzsIJaWpG-AkWyU,\"token\": \"ptok:v1:EsBQJIpLdLMq803a3BbrDb_FmNJIAGjT09wyCeLZ7OE>", +"\"[SAD-MASKED]\"https://[SECRET-MASKED]@h.example/ (350 msabc123\"350 mspassword=", +"\\https://[SECRET-MASKED]@h.example/", +",["the cvv is ptok:v1:uWivpUz4ccwadzczzDqfbKX8Hc8q0iHea90LngYZxRY", +"https://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@h/411111******1111\"token\": \"}"the cvv is Bearer password=", +"(cvv [SAD-MASKED]cvv=ptok:v1:q-aBDA-3PLgNB0wyeH6tCsUcbaLAm6sTRaHYrmp7nGI:ptok:v1:uWivpUz4ccwadzczzDqfbKX8Hc8q0iHea90LngYZxRY", +"ptok:v1:kVcmxuM7xSNaFxT-GvmeIwsb_FQMPsR72OBNUiZEAQccvv=350 ms", +null, +"{password=ptok:v1:5pID2SxgTGXwD6lym8bDTA4DjsmCC_0pZ9Pz4otwJ0A cvv is ptok:v1:4pnuHHw8Ta2XhDTSM67paaOyU_FIGzsIJaWpG-AkWyU{abc123123-45-6789123-45-6789", +"[SAD-MASKED]password='}", +null, +"ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAtoken=payment_id=ptok:v1:1bquJqVo3srNaRbuG70T9z6UbcmCgU5hKhrPyLm3VKUcvv ", +"]ptok:v1:q-aBDA-3PLgNB0wyeH6tCsUcbaLAm6sTRaHYrmp7nGI(ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\"https://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@h/]", +"Authorization: ptok:v1:Zekuypmh6yaS1AsQvDsvTlyFOrYxCFerqu1nVu19pvM", +"\"token\": \"ptok:v1:OgntccXlyQyW_nL_x0YrSu-X58RMe9zvBVdgzO8rDNA&password=ptok:v1:DvQKTJwXw4I8OCsOnchudoJOZPrAqtbZJvgg7g9bCy8(Authorization: ptok:v1:evPjFdIK1Ne_NCz6OPyEabtAWlfyfdmpbnmxKeU6Gpc", +"123|(411111******1111cvv [CVV-MASKED]", +"|123-45-6789}cvv=the cvv is [SAD-MASKED] 200", +"|https://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@h/\"[SAD-MASKED]\"{", +"{\"[SAD-MASKED]\"abc123", +null, +":\"[SAD-MASKED]\"cvv=https://[SECRET-MASKED]@h.example/payment_id=ptok:v1:tqMD0QTHoioAuEJh2YH9I44CbKcveX9vE5ZsDIUvQ7c cvv is ptok:v1:uWivpUz4ccwadzczzDqfbKX8Hc8q0iHea90LngYZxRY", +"][411111******1111GB33BUKB20201555555555", +null, +null, +"|'(:)[SAD-MASKED]{[ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", +":\"[SAD-MASKED]\"}123412/27200GB33BUKB20201555555555'", +null, +"\"token\": \"ptok:v1:5RkPBwJmQpSnPW3s96Hx64R40DfXrcgCOC7_t9KkkJk[SAD-MASKED]411111******1111}123-45-6789", +"cvv [CVV-MASKED]\"[SAD-MASKED]\"|12/27{ptok:v1:rx9ymYtEy0E6h4r1Fhu3usDzdIRy_u2Cvkap_65Q1FA ms", +"+96550000000token=Authorization: ptok:v1:ZiIw3nqLLx-8X4kmmh8eycCZAFu3Vw8KPm3lu36qCvw cvv is \"token\": \"", +"200payment_id=ptok:v1:xeqKeXDfAgkJu1u3l7v62xNWnzwv_ZolHM-PfvhaJQU=:\"&", +null, +"Bearer 350 ms123-45-6789:ptok:v1:aYJdn4WHn22T9DxSOhz71PawxIAImL3uKKs0MVy3nMM ", +"ptok:v1:4pnuHHw8Ta2XhDTSM67paaOyU_FIGzsIJaWpG-AkWyU(https://[SECRET-MASKED]@h.example/:]ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA123412/27the cvv is ", +"cvv token=ptok:v1:PReRrfIu9TdFf__-XxasmlOr2g-HkkQD2g2NsB84Zo8", +null, +"s3cr3tpassword='payment_id=ptok:v1:1bquJqVo3srNaRbuG70T9z6UbcmCgU5hKhrPyLm3VKUabc123GB33BUKB20201555555555", +"ptok:v1:0YjMxUc60VM5N5J2wzxEYLYAZl6vAWL-MjHkj0o3pJo******1111GB33BUKB20201555555555abc123123\"[SAD-MASKED]\"cvv ", +null, +"ptok:v1:kVcmxuM7xSNaFxT-GvmeIwsb_FQMPsR72OBNUiZEAQc", +"https://[SECRET-MASKED]@h.example/123the cvv is }", +"|payment_id=ptok:v1:9fRg7IhpBA5q_F8oRU8bLDs3vgcg44EMpX8VcTCNiuk350 mscvv=", +"'s3cr3thttps://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@h/[password=[SECRET-MASKED]:'", +"password=ptok:v1:1l8n_y53HKMEMBLPO5ry92SiSdJvJ-cjdnCeJ3LWuHA cvv='jane@example.comGB33BUKB20201555555555", +null, +"411111******1111s3cr3tGB33BUKB20201555555555 cvv=", +"]411111******1111123-45-6789cvv=411111******1111[SAD-MASKED])", +"ptok:v1:0YjMxUc60VM5N5J2wzxEYLYAZl6vAWL-MjHkj0o3pJo******111112/27350 ms:{350 ms", +null, +"|ptok:v1:uWivpUz4ccwadzczzDqfbKX8Hc8q0iHea90LngYZxRY", +"\\Bearer payment_id=ptok:v1:rP57Od-V4mWk_oZOWI56LI82gDruNastvPfqnu2ERxA\"token\": \"]", +"411111******1111https://u:p@h/411111******1111\\" 200", +null, +null, +"411111********1112/27GB33BUKB20201555555555", +""cvv [CVV-MASKED]\"[SAD-MASKED]\"\"[SAD-MASKED]\"\"Authorization: ", +"ptok:v1:DvQKTJwXw4I8OCsOnchudoJOZPrAqtbZJvgg7g9bCy8abc123,200350 ms\"token\": \"[", +"[Bearer [SECRET-MASKED]", +"\\123-45-6789https://u:p@h/https://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@h/", +"411111******1111cvv \\]", +"&350 ms&',Authorization: ptok:v1:XF_u3vLQRcTylgYgpdOmueKaFPsDokaheBcKp4DZH40", +"&123-45-6789Bearer https://[SECRET-MASKED]@h.example/]200jane@example.comGB33BUKB20201555555555abc123", +")ptok:v1:UoGOZOZipLeBOQc4K2s1IUU09YK1XVLK7WXRdS7HO-o cvv is ", +"}ptok:v1:uWivpUz4ccwadzczzDqfbKX8Hc8q0iHea90LngYZxRY", +null, +"\"[SAD-MASKED]\"411111******1111'[the cvv is payment_id=ptok:v1:xeqKeXDfAgkJu1u3l7v62xNWnzwv_ZolHM-PfvhaJQU=token=", +null, +"(} 411111******1111", +null, +"jane@example.com350 ms\"[SAD-MASKED]\"\"ptok:v1:uWivpUz4ccwadzczzDqfbKX8Hc8q0iHea90LngYZxRY&", +" ptok:v1:q-aBDA-3PLgNB0wyeH6tCsUcbaLAm6sTRaHYrmp7nGI+96550000000cvv=&]", +"\"ptok:v1:kVcmxuM7xSNaFxT-GvmeIwsb_FQMPsR72OBNUiZEAQc[SAD-MASKED]|[SAD-MASKED] (", +null, +"\"token=ptok:v1:0uZY6ffYA3KpsryjU3MqEO7mfbMhE-bQ5ZSkj4BAO4A>ptok:v1:DvQKTJwXw4I8OCsOnchudoJOZPrAqtbZJvgg7g9bCy8", +"123-45-678912/27eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.c2lnptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[SAD-MASKED]\"token\": \"[SECRET-MASKED]", +null, +"+96550000000cvv ptok:v1:uWivpUz4ccwadzczzDqfbKX8Hc8q0iHea90LngYZxRY\\https://[SECRET-MASKED]@h.example/", +null, +"[SAD-MASKED]", +"payment_id=ptok:v1:1bquJqVo3srNaRbuG70T9z6UbcmCgU5hKhrPyLm3VKU", +"Authorization: [SECRET-MASKED] ms", +"}|ptok:v1:uWivpUz4ccwadzczzDqfbKX8Hc8q0iHea90LngYZxRY", +"]350 ptok:v1:TCzvVbUyYQDxRU4pNFPDFOlsq2cEejMmhK351T9L1Xg=s3cr3teyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.c2ln", +"ptok:v1:4pnuHHw8Ta2XhDTSM67paaOyU_FIGzsIJaWpG-AkWyU|ptok:v1:7EOdctokf-ZR8FCj6iYp_odRy_RG0CWvKp6c5SAfXeA@example.com123+96550000000+96550000000[", +" ptok:v1:7yztZX2K_RJ5YNFazxRSSBP59fHRGiLB7ZmeZ7QAZ4Y&abc123ptok:v1:8MZB0mw2lBRaQkk0bEpv4gXCa3W9TPfz3_JdOnvhkxI:", +"(token=ptok:v1:RRa_cZrQaFwR42E7-XYXj212GHHF43_9AnNancpWMbc>(password=the cvv is ptok:v1:4pnuHHw8Ta2XhDTSM67paaOyU_FIGzsIJaWpG-AkWyU", +"payment_id=ptok:v1:1bquJqVo3srNaRbuG70T9z6UbcmCgU5hKhrPyLm3VKU(:}411111******1111}", +"200https://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@h/ the cvv is https://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@h/ptok:v1:DvQKTJwXw4I8OCsOnchudoJOZPrAqtbZJvgg7g9bCy8)password=", +"Bearer [SECRET-MASKED][SAD-MASKED]", +null, +"[SAD-MASKED]|411111******1111{[SECRET-MASKED]payment_id=ptok:v1:0cCkWj59PqqD1nCSM96T7bvjC_zc7o-9jjannna-xVA******1111token=", +"12/27\"[SAD-MASKED]\"350 ms123-45-6789[", +"\"[SAD-MASKED]\"ptok:v1:7yztZX2K_RJ5YNFazxRSSBP59fHRGiLB7ZmeZ7QAZ4Ythe cvv is ]", +null, +"'the cvv is https://[SECRET-MASKED]@h.example/"cvv=", +"ptok:v1:_dsmHy3z6GB3FHbxNVN2jIp_YMk1kjcK6ytMHqqdxmU=)", +null, +null, +null, +"token=\"[SAD-MASKED]\"token=", +"': ptok:v1:q-aBDA-3PLgNB0wyeH6tCsUcbaLAm6sTRaHYrmp7nGI{https://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@h/", +"200&:\"[SAD-MASKED]\""411111******1111the cvv is \"[SAD-MASKED]\"", +"]the cvv is ptok:v1:7yztZX2K_RJ5YNFazxRSSBP59fHRGiLB7ZmeZ7QAZ4Y", +"https://[SECRET-MASKED]@h.example/[cvv s3cr3t\"", +":(|the cvv is password=[SECRET-MASKED]>", +null, +null, +"ptok:v1:DvQKTJwXw4I8OCsOnchudoJOZPrAqtbZJvgg7g9bCy8'(\\https://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@h/&", +null, +"123456********6789{350 msabc123[", +" (password=ptok:v1:8fS9A2OE2Up7bcxCCyRtQpsesC7JmTY53o50FtfSj4Y ", +",ptok:v1:uWivpUz4ccwadzczzDqfbKX8Hc8q0iHea90LngYZxRY", +"]|\"token\": \"ptok:v1:FvClcU0VwPVODVVe5NWLakuHz4bZ4fWR6U0U7ZHLJrA,|123-45-6789payment_id=abc12345", +"{ptok:v1:DvQKTJwXw4I8OCsOnchudoJOZPrAqtbZJvgg7g9bCy8\"token\": \"ptok:v1:sRxfpE-CGkxp9cDGZ5HO2EYXm5nbKPDEpUQYE49PYLA cvv=}200", +"12/27\\|https://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@h/ptok:v1:4pnuHHw8Ta2XhDTSM67paaOyU_FIGzsIJaWpG-AkWyU\"token\": \"", +"]411111******1111\\", +null, +"Authorization: ptok:v1:YO091qKopha2KD9PBKapsFeE3DR1AJQe-BuTP4Vs1tc", +"123 ptok:v1:HLmb8YzgoK1U35ZfKaZGFc_r95nm6aLiKq6kqALW8sY=", +"411111******1111s3cr3t1234}cvv ,}cvv=ptok:v1:kVcmxuM7xSNaFxT-GvmeIwsb_FQMPsR72OBNUiZEAQc", +"123ptok:v1:DvQKTJwXw4I8OCsOnchudoJOZPrAqtbZJvgg7g9bCy8cvv password="s3cr3thttps://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@h/token=ptok:v1:Q_I98ohtQOorY_mzbIOseTxSA4OJvtRld_pkqXhuMWo", +"&)\"411111******1111\\[", +":payment_id=ptok:v1:1bquJqVo3srNaRbuG70T9z6UbcmCgU5hKhrPyLm3VKU}https://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@h/password=\"", +"123-45-6789https://[EMAIL-MASKED]/411111******1111", +"ptok:v1:q-aBDA-3PLgNB0wyeH6tCsUcbaLAm6sTRaHYrmp7nGI\"[SAD-MASKED]\" ", +"ptok:v1:DvQKTJwXw4I8OCsOnchudoJOZPrAqtbZJvgg7g9bCy8]\"token\": \"&abc123\\the cvv is the cvv is ptok:v1:7yztZX2K_RJ5YNFazxRSSBP59fHRGiLB7ZmeZ7QAZ4Y(", +"\\ptok:v1:q-aBDA-3PLgNB0wyeH6tCsUcbaLAm6sTRaHYrmp7nGI,cvv=,)", +null, +"'350 mshttps://[SECRET-MASKED]@h.example/'\\the cvv is ", +"https://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@h/password=\"ptok:v1:x5tD1gwqkgd68kO_KuLlX_HmFctvM8NGcKM90JidqYA&123", +"\\cvv \"[SAD-MASKED]\"", +"411111******1111https://u:p@h/password=", +"https://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@h/ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAuthorization: 4111111111111111123412/27Bearer [SECRET-MASKED]", +": GB33BUKB20201555555555eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.c2ln ptok:v1:DvQKTJwXw4I8OCsOnchudoJOZPrAqtbZJvgg7g9bCy8{ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", +"\",cvv |abc123ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA123-45-6789ptok:v1:DvQKTJwXw4I8OCsOnchudoJOZPrAqtbZJvgg7g9bCy8", +null, +"\"|411111******1111}411111******1111[SECRET-MASKED]", +": ]cvv=https://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@h/", +"\"[SAD-MASKED]\")}", +"https://[SECRET-MASKED]@h.example/:the cvv is Bearer &the cvv is \"ptok:v1:q-aBDA-3PLgNB0wyeH6tCsUcbaLAm6sTRaHYrmp7nGI+96550000000", +null, +"password=ptok:v1:nPAg-_KQLK8gaimtL3BFCAwg8tnfVdZQjoPiXotRl7s],password=", +null, +"12/27Bearer 123Authorization: password=ptok:v1:OgntccXlyQyW_nL_x0YrSu-X58RMe9zvBVdgzO8rDNA[SECRET-MASKED]abc123Bearer payment_id=ptok:v1:1bquJqVo3srNaRbuG70T9z6UbcmCgU5hKhrPyLm3VKU", +null, +null, +"ptok:v1:4pnuHHw8Ta2XhDTSM67paaOyU_FIGzsIJaWpG-AkWyU", +null, +"token=ptok:v1:ZiIw3nqLLx-8X4kmmh8eycCZAFu3Vw8KPm3lu36qCvw cvv is ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAhttps://[SECRET-MASKED]@h.example/Bearer cvv ", +null, +"\\)\"[SAD-MASKED]\"\\the cvv is ptok:v1:DvQKTJwXw4I8OCsOnchudoJOZPrAqtbZJvgg7g9bCy8", +null, +"|the cvv is https://[SECRET-MASKED]@h.example/ cvv ", +"ptok:v1:q-aBDA-3PLgNB0wyeH6tCsUcbaLAm6sTRaHYrmp7nGI'\"|\"411111******1111abc1231234s3cr3t", +null, +"Authorization: ptok:v1:DrCM8xehZjcWgStLT0YD4-0mHb3ezHcqvI4qH3-3Ndk&", +"payment_id=ptok:v1:1bquJqVo3srNaRbuG70T9z6UbcmCgU5hKhrPyLm3VKUptok:v1:uWivpUz4ccwadzczzDqfbKX8Hc8q0iHea90LngYZxRY\"[SAD-MASKED]\"https://[SECRET-MASKED]@h.example/Bearer \\", +" payment_id=ptok:v1:1bquJqVo3srNaRbuG70T9z6UbcmCgU5hKhrPyLm3VKU", +"Authorization: [SECRET-MASKED][SAD-MASKED]"&", +null, +"[SAD-MASKED]200s3cr3t123", +"(ptok:v1:DvQKTJwXw4I8OCsOnchudoJOZPrAqtbZJvgg7g9bCy8]password=ptok:v1:X7nJGQEn9vf8dLqRkv5ByDkGcSNP_64tcOykdQFsK3Yhttps://[SECRET-MASKED]@h.example/", +null, +null, +"\"token\": \"ptok:v1:VBlRNHeBVAYTDo1JiNzrMka8V9J7Ud_zIWudwd4nMAI ms12/27350 mseyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.c2ln", +"&+96550000000]https://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@h/411111******1111{\"", +"ptok:v1:kVcmxuM7xSNaFxT-GvmeIwsb_FQMPsR72OBNUiZEAQc\"[SECRET-MASKED]'s3cr3t\"411111******1111GB33BUKB20201555555555s3cr3t[SECRET-MASKED]", +"https://[SECRET-MASKED]@h.example/'{\\", +null, +"ptok:v1:0YjMxUc60VM5N5J2wzxEYLYAZl6vAWL-MjHkj0o3pJo******1111", +"ptok:v1:WA-jniwe6G3jZGnRKp8UvRjIQaUmo9pxBgxlCIDYzQs ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA350 msAuthorization: https://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@h/411111******1111", +null, +"[,Authorization: ptok:v1:uLmgaYjqjKRFlj5_KrJvP4jRH_4srfk8Mww59yXyg-4 ", +"411111******1111ptok:v1:DvQKTJwXw4I8OCsOnchudoJOZPrAqtbZJvgg7g9bCy8200cvv 411111******1111\"12/27https://u:p@h/", +"the cvv is (s3cr3t'https://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@h/ptok:v1:UoGOZOZipLeBOQc4K2s1IUU09YK1XVLK7WXRdS7HO-o cvv is \"token\": \"ptok:v1:jiUXyXo6tuUE0YobQ2OqEoMLzftfJ1u-h0lDOrSPYRM[SAD-MASKED]", +"\"ptok:v1:q-aBDA-3PLgNB0wyeH6tCsUcbaLAm6sTRaHYrmp7nGI:411111******1111", +null, +"'cvv=ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA350 mshttps://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@h/ https://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@h/123", +"{the cvv is ptok:v1:WA-jniwe6G3jZGnRKp8UvRjIQaUmo9pxBgxlCIDYzQs=200"200}", +"[SAD-MASKED]\\\\", +"ptok:v1:DvQKTJwXw4I8OCsOnchudoJOZPrAqtbZJvgg7g9bCy8 ]{s3cr3t]|[SAD-MASKED]", +"Bearer )411111******1111'ptok:v1:4pnuHHw8Ta2XhDTSM67paaOyU_FIGzsIJaWpG-AkWyU", +"411111******1111])}https://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@h/", +"https://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@h/Authorization: ptok:v1:sRxfpE-CGkxp9cDGZ5HO2EYXm5nbKPDEpUQYE49PYLA |[SECRET-MASKED]200350 ms", +"200411*********1111411111******1111\"ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAhttps://[EMAIL-MASKED]/ \"[SAD-MASKED]\"{", +null, +"411111******1111eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.c2ln,\"[SAD-MASKED]\"ptok:v1:4pnuHHw8Ta2XhDTSM67paaOyU_FIGzsIJaWpG-AkWyU|\\", +null, +"Authorization: )'('ptok:v1:DvQKTJwXw4I8OCsOnchudoJOZPrAqtbZJvgg7g9bCy8the cvv is the cvv is "", +"][SAD-MASKED]&", +"Authorization: ,https://[SECRET-MASKED]@h.example/", +"1234cvv=password=\"ptok:v1:r2HMq_CFEIsYZySf_MlUxRN1WoQRk0vAmUnzLSPnyDA\": \"", +"12/27\"[SAD-MASKED]\"Authorization: ", +" s3cr3tpayment_id=ptok:v1:1bquJqVo3srNaRbuG70T9z6UbcmCgU5hKhrPyLm3VKU", +"{Authorization: 200(ptok:v1:DvQKTJwXw4I8OCsOnchudoJOZPrAqtbZJvgg7g9bCy8ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", +"payment_id=ptok:v1:jJen5FbT_gS3TGxDDIiTVUh_Ibr0RMaexJHgO7yePA8/27 ", +null, +"https://[SECRET-MASKED]@h.example/cvv=ptok:v1:bPyqqfjBrHW8EncMmO_6a1DVXuNLbIfPPSzq78vuyNA/27token=12/27", +"]200https://[EMAIL-MASKED]/350 ms"", +null, +null, +""https://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@h/|ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", +"password=ptok:v1:DvQKTJwXw4I8OCsOnchudoJOZPrAqtbZJvgg7g9bCy8", +null, +null, +" (1234ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAtoken=\"[SAD-MASKED]\"cvv=jane@example.com200", +"[https://[SECRET-MASKED]@h.example/ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA411111******1111\"]", +null, +"\"token\": \"ptok:v1:y6ZP5NUfByC2a86RgdgXOVXw2uzjEsgp1L_9mHaqLe4\" [ptok:v1:DvQKTJwXw4I8OCsOnchudoJOZPrAqtbZJvgg7g9bCy8token=1234", +""123\"token\": \"ptok:v1:QAG_WrCjrJnyj1IJ-PHGRcovK_xSKvRZW40eB_-kjnA ", +"Bearer https://[SECRET-MASKED]@h.example/'", +"ptok:v1:4pnuHHw8Ta2XhDTSM67paaOyU_FIGzsIJaWpG-AkWyU\"[SAD-MASKED]\"https://[SECRET-MASKED]@h.example/\"token\": \"ptok:v1:1oqExnXK2I7lN5eJQl5EVtK1CKwLJM2ji6XHy3taI-I}ptok:v1:DvQKTJwXw4I8OCsOnchudoJOZPrAqtbZJvgg7g9bCy8abc123", +null, +null, +"Bearer ptok:v1:kmoFHE5jSsFXz2p_MwlYWKeaUtHAiI60F99guNncwB8>\\]\\", +null, +"https://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@h/cvv cvv [SAD-MASKED]'", +"[SECRET-MASKED]&411111******1111&https://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@h/\\[SECRET-MASKED]", +"}"cvv=][SECRET-MASKED][SECRET-MASKED]token=ptok:v1:MyMjFior5uA-BjPeGQy76ee0H5JiRSnT-yVjTBcKmWk 411111******1111", +"ptok:v1:DvQKTJwXw4I8OCsOnchudoJOZPrAqtbZJvgg7g9bCy8", +")https://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@h/", +null, +"[SAD-MASKED]cvv ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAptok:v1:UoGOZOZipLeBOQc4K2s1IUU09YK1XVLK7WXRdS7HO-o cvv is }abc123", +"Authorization: ptok:v1:qy2TOKFCtjNvRVgeGRo8qp2-K7jO9x9Kyf5SiWSi-6M", +"cvv token=ptok:v1:Af2mNlLZhb6ZHEA7zoc4JRQovNesGA7o4VG8oDD_iDM******1111https://[EMAIL-MASKED]/:[EMAIL-MASKED]", +"\"[SAD-MASKED]\"cvv cvv=[SECRET-MASKED]jane@example.com411111******1111", +"s3cr3t[SAD-MASKED]cvv=\\1234", +"Bearer (\"[SAD-MASKED]\"", +"ptok:v1:7yztZX2K_RJ5YNFazxRSSBP59fHRGiLB7ZmeZ7QAZ4Y{[ptok:v1:q-aBDA-3PLgNB0wyeH6tCsUcbaLAm6sTRaHYrmp7nGI:Bearer "", +null, +"ptok:v1:GgzasaQB979AO94bVeEcx9qS_jUhcG6CYx5QW6ymDdM\"token\": \"ptok:v1:OgntccXlyQyW_nL_x0YrSu-X58RMe9zvBVdgzO8rDNA[SAD-MASKED][SAD-MASKED],"]", +"https://[SECRET-MASKED]@h.example/token=ptok:v1:lyZxRgpOd9pLhGzGaBIdWyXkxCP6CisOOxLl8Pb6TNs ms]https://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@h/\"|{password=", +"[&123-45-6789411111******1111+96550000000[SAD-MASKED]abc123GB33BUKB20201555555555", +"[SECRET-MASKED]411111******1111"cvv=ptok:v1:uWivpUz4ccwadzczzDqfbKX8Hc8q0iHea90LngYZxRYptok:v1:UoGOZOZipLeBOQc4K2s1IUU09YK1XVLK7WXRdS7HO-o cvv is ", +"[SECRET-MASKED]"Authorization: \"ptok:v1:r2HMq_CFEIsYZySf_MlUxRN1WoQRk0vAmUnzLSPnyDA\": \"", +null, +"cvv [CVV-MASKED][SAD-MASKED] Authorization: ptok:v1:Zekuypmh6yaS1AsQvDsvTlyFOrYxCFerqu1nVu19pvM", +null, +"\"[SAD-MASKED]\"&", +"Bearer }s3cr3tBearer [CVV-MASKED]", +"1234[SECRET-MASKED]\"[SAD-MASKED]\"ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAthe cvv is ", +"\"[SAD-MASKED]\"the cvv is 411111******1111'", +null, +null, +null, +"ptok:v1:q-aBDA-3PLgNB0wyeH6tCsUcbaLAm6sTRaHYrmp7nGI,", +"ptok:v1:uWivpUz4ccwadzczzDqfbKX8Hc8q0iHea90LngYZxRY\"[SECRET-MASKED]https://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@h/", +"|"&ptok:v1:DvQKTJwXw4I8OCsOnchudoJOZPrAqtbZJvgg7g9bCy8]350 ms123-45-6789", +"Bearer \"[SAD-MASKED]\"200\"[SAD-MASKED]\",:ptok:v1:kVcmxuM7xSNaFxT-GvmeIwsb_FQMPsR72OBNUiZEAQc}abc123", +"token=ptok:v1:vCFcGO1n1078u72CH6TnfJ8c9hwfe3dYnuZObOjbUTE", +null, +"(https://[SECRET-MASKED]@h.example/]ptok:v1:uWivpUz4ccwadzczzDqfbKX8Hc8q0iHea90LngYZxRY", +"Authorization: [CVV-MASKED]", +"(\"[SAD-MASKED]\"411111******11111234payment_id=abc12345", +null, +")}https://[SECRET-MASKED]@h.example/ptok:v1:DvQKTJwXw4I8OCsOnchudoJOZPrAqtbZJvgg7g9bCy8https://u:p@h/'ptok:v1:q-aBDA-3PLgNB0wyeH6tCsUcbaLAm6sTRaHYrmp7nGI[SECRET-MASKED]+96550000000123-45-6789", +"\"[SAD-MASKED]\"200eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.c2ln[SECRET-MASKED]cvv ", +" the cvv is 411111******1111\"}Bearer [SAD-MASKED]", +"}Bearer token=[SECRET-MASKED])", +null, +"s3cr3ttoken=ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[SAD-MASKED]cvv=,password=", +"ptok:v1:4pnuHHw8Ta2XhDTSM67paaOyU_FIGzsIJaWpG-AkWyU", +"********9200)s3cr3t(Bearer [SECRET-MASKED]", +"411111*********1123-45-6789[1234password= cvv=token=\"ptok:v1:OgntccXlyQyW_nL_x0YrSu-X58RMe9zvBVdgzO8rDNA", +"https://[SECRET-MASKED]@h.example/ptok:v1:q-aBDA-3PLgNB0wyeH6tCsUcbaLAm6sTRaHYrmp7nGI", +"password="411111******1111:payment_id=ptok:v1:ZhrqLfejNr2HDpkR5P7kbFxOTg9-MIZhyxhhm8x499I mscvv=\"cvv=", +"abc123https://ptok:v1:4b7bLKI-Go6t0Jqtr8KU0Z2Q5h5y9r89K53J-6GMwO0:ptok:v1:InvxmX1HSYQYKfPCJJgnYhFZchiTLrb--KyIbSSzuZw@h/Bearer &(", +"[SAD-MASKED]]\"Authorization: ptok:v1:YgbfcFSHZJWtVqdJ5WJj1XbbjlVuK6iakCU4Ub9wo2w" +], +"keyset/redact_body":[ +null, +null, +null, +null, +null, +null, +null, +"\"token\": \"ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo\"", +"\"token\":\"ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo\"", +null, +null, +null, +"token=ptok:v1:k2xAAusWNcM1cNs3tP0FteIWgAUfUx_8aRQXn5zZrdo", +"token=ptok:v1:7ZWOgZZFY3UYwHRVga6PD6VriwK0ettMsFZOMt0d2IY", +"token=ptok:v1:JuuCkMnRlUJr5bdNk7w-vsvpb9mqq7UBGofK_xBj-0I", +null, +null, +null, +"token=ptok:v1:_lXByvUrQgAiZ4EOeOXGT_PlbUd0M_GNzfg43nNBFX4", +null, +"token=ptok:v1:OGFRjEzuVhZrn1tlwWeQ5zNvk4_khFjSFKrMQY_YIME", +null, +null, +null, +"message token=ptok:v1:7ZWOgZZFY3UYwHRVga6PD6VriwK0ettMsFZOMt0d2IY", +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +"access_token=ptok:v1:TSsQJnxs4gq-u3wovFm24skGr5SOOxZJDuZcby0wbNk", +"refresh_token=ptok:v1:TSsQJnxs4gq-u3wovFm24skGr5SOOxZJDuZcby0wbNk", +"client_secret=ptok:v1:1Uc-bob3ywEPV_TX7RuoxshPDdrvBnjpYJK7cp-hYYY", +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +"CVV=[CVV-MASKED]", +"Here cvv=[CVV-MASKED] is submitted", +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +"", +"error: client_secret=\"[SECRET-MASKED]\" rejected", +"", +"", +"{\"password\": \"[SECRET-MASKED]\"", +"{\"id\": 1, \"password\": \"[SECRET-MASKED]\"", +"password=[SECRET-MASKED], status=ok", +"password=[SECRET-MASKED]", +"password=[SECRET-MASKED]", +"password=[SECRET-MASKED]&x=1", +"password=[SECRET-MASKED]&x=1", +"password=\"[SECRET-MASKED]\"&x=1", +"password=[SECRET-MASKED]&x=1", +"{\"xml\": \"\"}", +"{\"note\": \"password=\\\"[SECRET-MASKED]\\\"&x=1\"}", +"{\"note\": \"password=[SECRET-MASKED]&x=1\"}", +"{\"password\": \"[SECRET-MASKED]\"}", +"{\"url\": \"https://x?secret=[SECRET-MASKED]\"}", +"{\"password\": \"[SECRET-MASKED]\"}", +null, +null, +null, +null, +null, +null, +null, +null, +"sent {\"password\": \"ptok:v1:ACviYdsWVCvlWaJzzKIvSwsM3M5gbAOH6xO6aPy_c7I\"} to the gateway", +"login with password=ptok:v1:ACviYdsWVCvlWaJzzKIvSwsM3M5gbAOH6xO6aPy_c7I failed", +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +"cvv=[CVV-MASKED] for 411111******1111", +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +"TRANPORTAL123ptok:v1:AQ6W6lJiUPhhfswZOx3SLOQ5DZ_fAoBspUjCrPV_4lA110.000TRK1{\"paymentData\":\"[SAD-MASKED]\"}", +"TRANPORTAL123ptok:v1:AQ6W6lJiUPhhfswZOx3SLOQ5DZ_fAoBspUjCrPV_4lA810.000202612345TRK1TrackID", +"TRANPORTAL123ptok:v1:AQ6W6lJiUPhhfswZOx3SLOQ5DZ_fAoBspUjCrPV_4lA25.000202612345TRK1TrackID", +null, +null, +"[SAD-MASKED]jane@example.com]", +null, +null, +"123-45-6789350 mshttps://4111111111111111@h.example/[SAD-MASKED],", +"Bearer |GB33BUKB20201555555555\"token\": \"\"", +"123cvv=[CVV-MASKED](", +null, +null, +null, +"(eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.c2ln+96550000000token=jane@example.com,{\"[SAD-MASKED]\"12/27", +null, +null, +"https://u:p@h/abc123cvv=[CVV-MASKED] |", +null, +null, +null, +"350 ms\\token=ptok:v1:VG2_PhmHDVXo4dU7Be0YY5yYcpbyPZ379gLFvrwfZFE \"token\": \"\"", +"1234Bearer \"https://u:p@h/[SAD-MASKED]\"token\": \"\"", +"[CVV-MASKED](", +"200\"[SAD-MASKED]\"12/27)jane@example.com123411111111111111112/27200", +null, +null, +"123[cvv=[CVV-MASKED]", +",(\"[SAD-MASKED]\"Authorization: +96550000000", +"GB33BUKB20201555555555,\"token\": \"ptok:v1:fFAsPjFOQVsQSECZ69zH7Um5i5pLfIQTzqLoS3w1vmk\"", +"\"[SAD-MASKED]\"https://4111111111111111@h.example/ (350 msabc123\"350 mspassword=", +null, +null, +"https://u:p@h/411111******1111\"token\": \"ptok:v1:K2vk0TzbgkpuSL1pMBMfaikLWpgq8whpQmc25thy9Bc\"", +"(cvv [SAD-MASKED]cvv=jane@example.com:eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.c2ln", +null, +null, +"{password=ptok:v1:5pID2SxgTGXwD6lym8bDTA4DjsmCC_0pZ9Pz4otwJ0A cvv is GB33BUKB20201555555555{abc123123-45-6789123-45-6789", +"[SAD-MASKED]password=ptok:v1:FDcjzaAEe8LfHN1wHWFoD2U34_9WFOspnE2VVLg1q0E}", +null, +null, +null, +null, +"\"token\": \"ptok:v1:joDGEHIusRRezTqS1E6-pR_kAxno9MXPzNei6sRnuis\"", +null, +"|123-45-6789}cvv=[CVV-MASKED] cvv is [SAD-MASKED] 200", +"|https://u:p@h/\"[SAD-MASKED]\"{", +"{\"[SAD-MASKED]\"abc123", +null, +":\"[SAD-MASKED]\"cvv=[CVV-MASKED] cvv is eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.c2ln", +null, +null, +null, +"|'(:)[SAD-MASKED]{[ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", +":\"[SAD-MASKED]\"}123412/27200GB33BUKB20201555555555'", +null, +"\"token\": \"[SECRET-MASKED]\"", +"cvv 411111******1111\"[SAD-MASKED]\"|12/27{GB33BUKB20201555555555350 ms", +"+96550000000token=Authorization: the cvv is \"token\": \"\"", +null, +null, +null, +null, +"cvv token=ptok:v1:eeW8k97Fp9OGLPLxTn-3pDxRE7BvoR5x-nLndG1v6p8 +96550000000", +null, +null, +"123411111******1111GB33BUKB20201555555555abc123123\"[SAD-MASKED]\"cvv ", +null, +null, +null, +null, +"'s3cr3thttps://u:p@h/[password=ptok:v1:uONjzgC49eDNVmANmX7TlgQnrqn6858U1qjC3PRVnUo'", +"password=ptok:v1:1l8n_y53HKMEMBLPO5ry92SiSdJvJ-cjdnCeJ3LWuHA cvv=[CVV-MASKED]jane@example.comGB33BUKB20201555555555", +null, +null, +"]411111******1111123-45-6789cvv=[CVV-MASKED]", +null, +null, +null, +"\\Bearer payment_id=abc12345123\"token\": \"ptok:v1:x2HUT5ZLR2yVzMMUEDFklXsu_6hQi6iXOpqKvXlb4hg\"", +null, +null, +null, +null, +""cvv 123\"[SAD-MASKED]\"\"[SAD-MASKED]\"\"Authorization: ", +"-----BEGIN RSA PRIVATE KEY-----\nMIIB\n-----END RSA PRIVATE KEY-----abc123,200350 ms\"token\": \"ptok:v1:eunVth1X_-uGPkR1badWo6tClaFR3ktA_q5llc47gUQ\"", +null, +null, +null, +null, +null, +null, +null, +null, +"\"[SAD-MASKED]\"411111******1111'[the cvv is payment_id=abc12345token=token=", +null, +null, +null, +"jane@example.com350 ms\"[SAD-MASKED]\"\"eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.c2ln&", +null, +"\"+96550000000[SAD-MASKED]|[SAD-MASKED] (", +null, +"\"token=ptok:v1:XnCWs2Y2uXYfAyp1j17QBshA_zuXm-AGaHIyPmMgcFI RSA PRIVATE KEY-----\nMIIB\n-----END RSA PRIVATE KEY-----", +"123-45-678912/27eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.c2lnptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[SAD-MASKED]\"token\": \"[SECRET-MASKED]\"", +null, +null, +null, +"[SAD-MASKED]", +null, +null, +null, +null, +null, +null, +"(token=ptok:v1:tOUvC-DIbCgIca0OEZueI-YgJatl7r8M1guB1lhTyU4 cvv is GB33BUKB20201555555555", +null, +null, +"Bearer https://4111111111111111@h.example/|[SAD-MASKED]", +null, +"[SAD-MASKED]|411111******1111{[SECRET-MASKED]payment_id=abc12345411111******1111token=", +"12/27\"[SAD-MASKED]\"350 ms123-45-6789[", +"\"[SAD-MASKED]\"123-45-6789the cvv is ]", +null, +null, +null, +null, +null, +null, +"token=\"ptok:v1:SnGIVazZ4Dg37KAjH3XS-F5qwyINxxDJDurw3enWudo", +null, +"200&:\"[SAD-MASKED]\""411111******1111the cvv is \"[SAD-MASKED]\"", +null, +null, +":(|the cvv is password=[SECRET-MASKED]>", +null, +null, +null, +null, +null, +" (password=ptok:v1:8fS9A2OE2Up7bcxCCyRtQpsesC7JmTY53o50FtfSj4Y ", +null, +"]|\"token\": \"ptok:v1:f2WZpStn3uU_8iz-P05UeerKHzNxD0RQo34y9OOkgyY\"", +"{-----BEGIN RSA PRIVATE KEY-----\nMIIB\n-----END RSA PRIVATE KEY-----\"token\": \"ptok:v1:gs9sYQhZrGxcSpJIyrcouB-0DnBgBI_tS7tcHC34kjE\"", +"12/27\\|https://u:p@h/GB33BUKB20201555555555\"token\": \"\"", +null, +null, +null, +null, +"411111******1111s3cr3t1234}cvv ,}cvv=[CVV-MASKED]", +"123-----BEGIN RSA PRIVATE KEY-----\nMIIB\n-----END RSA PRIVATE KEY-----cvv password="s3cr3thttps://u:p@h/token=ptok:v1:Q_I98ohtQOorY_mzbIOseTxSA4OJvtRld_pkqXhuMWo", +null, +null, +null, +"jane@example.com\"[SAD-MASKED]\" ", +"-----BEGIN RSA PRIVATE KEY-----\nMIIB\n-----END RSA PRIVATE KEY-----]\"token\": \"ptok:v1:FG1zsdkj33bK0uBrqSVnVHQcXtMjsbOUHeA8C2UHlEg\"", +"\\jane@example.com,cvv=[CVV-MASKED]>", +"2001234'[\"token\": \"ptok:v1:cSduqzTYZ7ls8r6qokkQvgcvE-KoNbfhTuNuWiOwwq4\"", +null, +"https://u:p@h/password=\"ptok:v1:x5tD1gwqkgd68kO_KuLlX_HmFctvM8NGcKM90JidqYA&123", +"\\cvv \"[SAD-MASKED]\"", +null, +null, +null, +null, +null, +null, +": ]cvv=[CVV-MASKED]", +"\"[SAD-MASKED]\")}", +null, +null, +"password=ptok:v1:EP-wzOm9xZpI0xUEzsF8lMafT5PXjceoBlNP-CBWrTA", +null, +"12/27Bearer 123Authorization: password=ptok:v1:6lBdi-Nn0JFBUVTvUEroxaRo9K3MxNvhYbhwqkgP48o payment_id=abc12345", +null, +null, +null, +"\\token=ptok:v1:xEg-8fduMmbsibdmdCT8ArshsABAfvNf6ANM8erDnOQ", +"token=ptok:v1:ZiIw3nqLLx-8X4kmmh8eycCZAFu3Vw8KPm3lu36qCvw cvv is ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAhttps://4111111111111111@h.example/Bearer cvv ", +null, +"\\)\"[SAD-MASKED]\"\\the cvv is -----BEGIN RSA PRIVATE KEY-----\nMIIB\n-----END RSA PRIVATE KEY-----", +null, +null, +null, +null, +null, +"payment_id=abc12345eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.c2ln\"[SAD-MASKED]\"https://4111111111111111@h.example/Bearer \\", +null, +"Authorization: [SECRET-MASKED][SAD-MASKED]"&", +null, +"[SAD-MASKED]200s3cr3t123", +"(-----BEGIN RSA PRIVATE KEY-----\nMIIB\n-----END RSA PRIVATE KEY-----]password=ptok:v1:X7nJGQEn9vf8dLqRkv5ByDkGcSNP_64tcOykdQFsK3Yhttps://4111111111111111@h.example/", +null, +null, +"\"token\": \"ptok:v1:z-sIY3QUEMMFgYqqrTMI0GsNI4aLULEOMaEocHoCpN4\"", +null, +null, +null, +null, +null, +null, +null, +null, +null, +"the cvv is (s3cr3t'https://u:p@h/jane@example.comthe cvv is \"token\": \"ptok:v1:c3KkSgx1cBu85kbXhxn1JVOxIYuUg9g0m9iPyZfyxjA\"", +null, +null, +"'cvv=[CVV-MASKED] mshttps://u:p@h/ https://u:p@h/123", +"{the cvv is jane@example.comcvv=[CVV-MASKED]"200}", +"[SAD-MASKED]\\\\", +"-----BEGIN RSA PRIVATE KEY-----\nMIIB\n-----END RSA PRIVATE KEY----- ]{s3cr3t]|[SAD-MASKED]", +null, +null, +null, +"20041111111111111114111111111111111\"ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAhttps://4111111111111111@h.example/ \"[SAD-MASKED]\"{", +null, +"411111******1111eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.c2ln,\"[SAD-MASKED]\"GB33BUKB20201555555555|\\", +null, +null, +"][SAD-MASKED]&", +null, +"1234cvv=[CVV-MASKED]\": \"", +"12/27\"[SAD-MASKED]\"Authorization: ", +null, +null, +null, +null, +"https://4111111111111111@h.example/cvv=[CVV-MASKED]", +null, +null, +null, +null, +"password=ptok:v1:1MqqmeMk7qoYT1Pv1i5UKSfj4QNiD9YfJIENADBDPo4 RSA PRIVATE KEY-----\nMIIB\n-----END RSA PRIVATE KEY-----", +"\\1234cvv=[CVV-MASKED]|123-45-6789", +null, +" (1234ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAtoken=\"[SAD-MASKED]\"cvv=jane@example.com200", +null, +null, +"\"token\": \"ptok:v1:y6ZP5NUfByC2a86RgdgXOVXw2uzjEsgp1L_9mHaqLe4\" [-----BEGIN RSA PRIVATE KEY-----\nMIIB\n-----END RSA PRIVATE KEY-----token=ptok:v1:1oqExnXK2I7lN5eJQl5EVtK1CKwLJM2ji6XHy3taI-I", +""123\"token\": \"ptok:v1:QAG_WrCjrJnyj1IJ-PHGRcovK_xSKvRZW40eB_-kjnA\"", +null, +"GB33BUKB20201555555555\"[SAD-MASKED]\"https://4111111111111111@h.example/\"token\": \"ptok:v1:213z7QQ33TCO87IBALUHwGhGfeH4_zbtJSBcU5cXvKA\"", +null, +null, +null, +null, +"https://u:p@h/cvv cvv [SAD-MASKED]'", +null, +"}"cvv=[CVV-MASKED] 4111111111111111", +null, +null, +null, +"[SAD-MASKED]cvv ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAjane@example.comthe cvv is }abc123", +null, +null, +"\"[SAD-MASKED]\"cvv cvv=[CVV-MASKED]>", +"s3cr3t[SAD-MASKED]cvv=[CVV-MASKED]", +"Bearer (\"[SAD-MASKED]\"", +null, +null, +"GB33BUKB20201555555555123\"token\": \"ptok:v1:fcLSDeAk-e3LcCFH20qDx4l1Qr_T6uQNarapORhCFM0\"", +"https://4111111111111111@h.example/token=ptok:v1:lyZxRgpOd9pLhGzGaBIdWyXkxCP6CisOOxLl8Pb6TNs ms]https://u:p@h/\"|{password=", +"[&123-45-6789411111******1111+96550000000[SAD-MASKED]abc123GB33BUKB20201555555555", +"[SECRET-MASKED]4111111111111111"cvv=[CVV-MASKED]jane@example.comthe cvv is ", +"[SECRET-MASKED]"Authorization: \"token\": \"\"", +null, +"cvv 123[SAD-MASKED] Authorization: eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.c2ln", +null, +"\"[SAD-MASKED]\"&", +"Bearer }s3cr3tBearer [CVV-MASKED]", +"1234[SECRET-MASKED]\"[SAD-MASKED]\"ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAthe cvv is ", +"\"[SAD-MASKED]\"the cvv is 411111******1111'", +null, +null, +null, +null, +null, +null, +"Bearer \"[SAD-MASKED]\"200\"[SAD-MASKED]\",:+96550000000}abc123", +"token=ptok:v1:fPCMDp9s1WEDJ40e671oOLz36gi3a5WaJnxfPRb65GY]"Bearer \"token\": \"\"", +null, +null, +"Authorization: [CVV-MASKED]", +"(\"[SAD-MASKED]\"41111111111111111234payment_id=abc12345", +null, +null, +"\"[SAD-MASKED]\"200eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.c2ln[SECRET-MASKED]cvv ", +" the cvv is 411111******1111\"}Bearer [SAD-MASKED]", +"}Bearer token=[SECRET-MASKED]", +null, +"s3cr3ttoken=ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[SAD-MASKED]cvv=,password=ptok:v1:fLGajcsOM72XzCpj-9v0rPxN7BXIR7cFOyQdlQQa_gw>", +null, +null, +"4111111111111111123-45-6789[1234password= cvv=[CVV-MASKED]", +null, +"password="4111111111111111:payment_id=abc12345350 mscvv=\"[CVV-MASKED]", +null, +"[SAD-MASKED]]\"Authorization: (jane@example.com123-45-6789&" +], +"no-keyset/default/events":[ +{ +"event":"api request received: POST /b/checkout/v1/pymt-txn/", +"http":{ +"request":{ +"body":"{\"session_id\": \"21a2496f8b6c193ec61b8dd52cbe8d827b4eaa49\", \"order_no\": \"ORD-2026-000123\", \"amount\": \"12.500\", \"currency_code\": \"KWD\", \"customer_email\": \"[EMAIL-MASKED]\", \"customer_phone\": \"[PHONE-MASKED]\", \"customer_first_name\": \"[NAME-MASKED]\", \"billing_address\": {\"line1\": \"[ADDRESS-MASKED]\", \"city\": \"[ADDRESS-MASKED]\", \"country\": \"[ADDRESS-MASKED]\"}, \"pg_codes\": [\"mpgs-direct\", \"cs-direct\", \"kpay\"], \"redirect_url\": \"https://merchant.example/return?ref=abc123&lang=en\", \"webhook_url\": \"https://merchant.example/hooks/ottu\", \"extra\": {\"note\": \"gift wrap\", \"items\": [{\"sku\": \"SKU-0\", \"qty\": 0}, {\"sku\": \"SKU-1\", \"qty\": 1}, {\"sku\": \"SKU-2\", \"qty\": 2}, {\"sku\": \"SKU-3\", \"qty\": 3}, {\"sku\": \"SKU-4\", \"qty\": 4}, {\"sku\": \"SKU-5\", \"qty\": 5}, {\"sku\": \"SKU-6\", \"qty\": 6}, {\"sku\": \"SKU-7\", \"qty\": 7}, {\"sku\": \"SKU-8\", \"qty\": 8}, {\"sku\": \"SKU-9\", \"qty\": 9}]}, \"card_acceptance_criteria\": {\"min_expiry_time\": 30}, \"three_ds\": {\"directory_server_transaction_id\": \"3d85c5d3-8412-4f1d-9e26-4a0f00123456\"}}", +"headers":{ +"authorization":"[SECRET-MASKED]", +"user-agent":"Mozilla/5.0" +} +} +}, +"payload":{ +"amount":"12.500", +"billing_address":{ +"city":"[ADDRESS-MASKED]", +"country":"[ADDRESS-MASKED]", +"line1":"[ADDRESS-MASKED]" +}, +"card_acceptance_criteria":{ +"min_expiry_time":30 +}, +"currency_code":"KWD", +"customer_email":"[EMAIL-MASKED]", +"customer_first_name":"[NAME-MASKED]", +"customer_phone":"[PHONE-MASKED]", +"extra":{ +"items":[ +{ +"qty":0, +"sku":"SKU-0" +}, +{ +"qty":1, +"sku":"SKU-1" +}, +{ +"qty":2, +"sku":"SKU-2" +}, +{ +"qty":3, +"sku":"SKU-3" +}, +{ +"qty":4, +"sku":"SKU-4" +}, +{ +"qty":5, +"sku":"SKU-5" +}, +{ +"qty":6, +"sku":"SKU-6" +}, +{ +"qty":7, +"sku":"SKU-7" +}, +{ +"qty":8, +"sku":"SKU-8" +}, +{ +"qty":9, +"sku":"SKU-9" +} +], +"note":"gift wrap" +}, +"order_no":"ORD-2026-000123", +"pg_codes":[ +"mpgs-direct", +"cs-direct", +"kpay" +], +"redirect_url":"https://merchant.example/return?ref=abc123&lang=en", +"session_id":"21a2496f8b6c193ec61b8dd52cbe8d827b4eaa49", +"three_ds":{ +"directory_server_transaction_id":"3d85c5d3-8412-4f1d-9e26-4a0f00123456" +}, +"webhook_url":"https://merchant.example/hooks/ottu" +}, +"url":{ +"full":"https://jade.ottu.dev/b/checkout/v1/pymt-txn/?utm=1" +} +}, +{ +"event":"pg.response_received from apitest.cybersource.com", +"http":{ +"request":{ +"method":"POST" +}, +"response":{ +"body":"{\"id\":\"pay_9f8e7d6c5b4a\",\"status\":\"AUTHORIZED\",\"amount\":\"100.000\",\"currency\":\"KWD\",\"processorInformation\":{\"approvalCode\":\"831000\",\"responseCode\":\"100\",\"transactionId\":\"7266500001234567\"},\"orderInformation\":{\"amountDetails\":{\"authorizedAmount\":\"100.000\",\"currency\":\"KWD\"}},\"_links\":{\"self\":{\"href\":\"/pts/v2/payments/7266500001234567\"}}}", +"status_code":201 +} +}, +"labels":{ +"direction":"outbound", +"namespace":"cybersource", +"operation":"payment", +"pg_result":"AUTHORIZED" +}, +"merchant_id":"jade.ottu.dev", +"payment":{ +"pg_code":"cs-direct", +"reference":"jade-oTTE45" +}, +"service":{ +"target":{ +"name":"cybersource" +} +}, +"session_id":"b1e2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0", +"url":{ +"full":"https://apitest.cybersource.com/pts/v2/payments/" +} +}, +{ +"event":"pg.response_received from apitest.cybersource.com", +"http":{ +"request":{ +"method":"POST" +}, +"response":{ +"body":"{\"access_token\": \"[SECRET-MASKED]\", \"id\": \"pay_9f8e7d6c5b4a\", \"status\": \"AUTHORIZED\", \"amount\": \"100.000\", \"currency\": \"KWD\", \"processorInformation\": {\"approvalCode\": \"831000\", \"responseCode\": \"100\", \"transactionId\": \"7266500001234567\"}, \"orderInformation\": {\"amountDetails\": {\"authorizedAmount\": \"100.000\", \"currency\": \"KWD\"}}, \"_links\": {\"self\": {\"href\": \"/pts/v2/payments/7266500001234567\"}}}", +"status_code":201 +} +}, +"labels":{ +"direction":"outbound", +"namespace":"cybersource", +"operation":"payment", +"pg_result":"AUTHORIZED" +}, +"merchant_id":"jade.ottu.dev", +"payment":{ +"pg_code":"cs-direct", +"reference":"jade-oTTE45" +}, +"service":{ +"target":{ +"name":"cybersource" +} +}, +"session_id":"b1e2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0", +"url":{ +"full":"https://apitest.cybersource.com/pts/v2/payments/" +} +}, +{ +"event":"pg.request_sent to MPGS", +"message_text":"card 4111 1111 1111 1111 cvv [CVV-MASKED] for [EMAIL-MASKED]", +"payload":{ +"customer":{ +"email":"[EMAIL-MASKED]", +"firstName":"[NAME-MASKED]" +}, +"order":{ +"amount":20, +"reference":"deltabRKJ5X_0" +}, +"sourceOfFunds":{ +"provided":{ +"card":{ +"expiry":{ +"month":"01", +"year":"27" +}, +"number":"411111******1111", +"securityCode":"[CVV-MASKED]" +} +} +} +}, +"session_id":"b1e2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0" +}, +{ +"event":"create payment", +"token":"[SECRET-MASKED]" +}, +{ +"cvv":"[CVV-MASKED]" +}, +{ +"cvv":"[CVV-MASKED]" +}, +{ +"session_key":"[SECRET-MASKED]" +}, +{ +"access_token":null +}, +{ +"card":null, +"customer_name":null, +"cvv":null, +"expiry":null, +"public_key":null +}, +{ +"Cvv":"[CVV-MASKED]" +}, +{ +"securityCode":"[CVV-MASKED]" +}, +{ +"data":{ +"token":{ +"more":"[SECRET-MASKED]", +"nested":"[SECRET-MASKED]" +} +} +}, +{ +"payload":{ +"note":"[SAD-MASKED]", +"paymentData":"[SAD-MASKED]" +} +}, +{ +"http":{ +"request":{ +"body":{ +"content":"TRANPORTAL123[SECRET-MASKED]110.000TRK1{\"paymentData\":\"[SAD-MASKED]\"}" +} +} +} +}, +{ +"http":{ +"request":{ +"body":{ +"content":"TRANPORTAL123[SECRET-MASKED]810.000202612345TRK1TrackID" +} +} +} +}, +{ +"http":{ +"request":{ +"body":{ +"content":"TRANPORTAL123[SECRET-MASKED]25.000202612345TRK1TrackID" +} +} +} +}, +{ +"event":"[PEM-KEY-MASKED]" +}, +{ +"event":"[PEM-KEY-MASKED]" +}, +{ +"event":"[PEM-KEY-MASKED]" +}, +{ +"event":"[PEM-KEY-MASKED]" +}, +{ +"event":"[PEM-KEY-MASKED]" +}, +{ +"event":"'token': '[SECRET-MASKED]'" +}, +{ +"event":"'token':'[SECRET-MASKED]'" +}, +{ +"event":"\"token\": \"[SECRET-MASKED]\"" +}, +{ +"event":"\"token\":\"[SECRET-MASKED]\"" +}, +{ +"event":"token: [SECRET-MASKED]" +}, +{ +"event":"token:[SECRET-MASKED]" +}, +{ +"event":"token= [SECRET-MASKED]" +}, +{ +"event":"token=[SECRET-MASKED]" +}, +{ +"event":"token=[SECRET-MASKED]" +}, +{ +"event":"token=[SECRET-MASKED]" +}, +{ +"event":"token: [SECRET-MASKED]" +}, +{ +"event":"token: [SECRET-MASKED]" +}, +{ +"event":"token:[SECRET-MASKED]" +}, +{ +"event":"token=[SECRET-MASKED]" +}, +{ +"event":"token:[SECRET-MASKED]" +}, +{ +"event":"token=[SECRET-MASKED]" +}, +{ +"event":"token [SECRET-MASKED]" +}, +{ +"event":"token [SECRET-MASKED]" +}, +{ +"event":"token [SECRET-MASKED]" +}, +{ +"event":"message token=[SECRET-MASKED]" +}, +{ +"event":"anyword_token [SECRET-MASKED]" +}, +{ +"event":"any_word_token [SECRET-MASKED]" +}, +{ +"event":"any-word_token [SECRET-MASKED]" +}, +{ +"event":"any-word-token [SECRET-MASKED]" +}, +{ +"event":"any_1-word_2-token [SECRET-MASKED]" +}, +{ +"event":"secret [SECRET-MASKED]" +}, +{ +"event":"any_1-word_2-secret [SECRET-MASKED]" +}, +{ +"event":"password [SECRET-MASKED]" +}, +{ +"event":"any_1-word_2-password [SECRET-MASKED]" +}, +{ +"event":"passwd [SECRET-MASKED]" +}, +{ +"event":"any_1-word_2-passwd [SECRET-MASKED]" +}, +{ +"event":"bearer [SECRET-MASKED]" +}, +{ +"event":"basic [SECRET-MASKED]" +}, +{ +"event":"digest [SECRET-MASKED]" +}, +{ +"event":"credential [SECRET-MASKED]" +}, +{ +"event":"credentials [SECRET-MASKED]" +}, +{ +"event":"authorization [SECRET-MASKED]" +}, +{ +"event":"authorization_header [SECRET-MASKED]" +}, +{ +"event":"authorization-header [SECRET-MASKED]" +}, +{ +"event":"authorisation [SECRET-MASKED]" +}, +{ +"event":"authorisation_header [SECRET-MASKED]" +}, +{ +"event":"authorisation-header [SECRET-MASKED]" +}, +{ +"event":"apikey [SECRET-MASKED]" +}, +{ +"event":"api_key [SECRET-MASKED]" +}, +{ +"event":"[SECRET-MASKED]" +}, +{ +"event":"secret-key [SECRET-MASKED]" +}, +{ +"event":"private-key [SECRET-MASKED]" +}, +{ +"event":"public-key [SECRET-MASKED]" +}, +{ +"event":"encryption-key [SECRET-MASKED]" +}, +{ +"event":"decryption-key [SECRET-MASKED]" +}, +{ +"event":"signing-key [SECRET-MASKED]" +}, +{ +"event":"access-key [SECRET-MASKED]" +}, +{ +"event":"master-key [SECRET-MASKED]" +}, +{ +"event":"root-key [SECRET-MASKED]" +}, +{ +"event":"session-key [SECRET-MASKED]" +}, +{ +"event":"secret_key=[SECRET-MASKED]" +}, +{ +"event":"'Authorization': '[SECRET-MASKED]'" +}, +{ +"event":"\"Authorization\": \"[SECRET-MASKED]\"" +}, +{ +"event":"Authorization: [SECRET-MASKED]" +}, +{ +"event":"Authorization= [SECRET-MASKED]" +}, +{ +"event":"Authorization Bearer [SECRET-MASKED]" +}, +{ +"event":"{\"Authorization\": \"[SECRET-MASKED]\"}" +}, +{ +"event":"Here is {\"Authorization\": \"[SECRET-MASKED]\"}" +}, +{ +"event":"{\"Authorization\": \"[SECRET-MASKED]\"}" +}, +{ +"event":"Authorization: [SECRET-MASKED]" +}, +{ +"event":"{\"Authorisation\": \"[SECRET-MASKED]\"}" +}, +{ +"event":"Authentication failed with key '[SECRET-MASKED]'" +}, +{ +"event":"access_token=[SECRET-MASKED]" +}, +{ +"event":"refresh_token=[SECRET-MASKED]" +}, +{ +"event":"client_secret=[SECRET-MASKED]" +}, +{ +"event":"private_key=[SECRET-MASKED]" +}, +{ +"event":"secret_key=[SECRET-MASKED]" +}, +{ +"event":"basic= [SECRET-MASKED]" +}, +{ +"event":"[SECRET-MASKED]" +}, +{ +"event":"Basic [SECRET-MASKED]" +}, +{ +"event":"body {\"public_key\": null, \"a\": 1}" +}, +{ +"event":"body {\"session_key\": true}" +}, +{ +"event":"body {'public_key': None}" +}, +{ +"event":"body {\"session_key\": \"[SECRET-MASKED]\"}" +}, +{ +"event":"body {'api_key': '[SECRET-MASKED]'}" +}, +{ +"event":"{\"result\": \"SUCCESS\", \"session\": {\"aes256Key\": \"[SECRET-MASKED]\", \"id\": \"SESS" +}, +{ +"event":"aes256Key=[SECRET-MASKED]" +}, +{ +"event":"hmacSha256Key=[SECRET-MASKED]" +}, +{ +"event":"privateKeyPem=[SECRET-MASKED]" +}, +{ +"event":"codes_key=abcd1234efgh5678" +}, +{ +"event":"'cvv': '[CVV-MASKED]'" +}, +{ +"event":"'Cvv':'[CVV-MASKED]'" +}, +{ +"event":"\"cVv\": \"[CVV-MASKED]\"" +}, +{ +"event":"\"cvV\":\"[CVV-MASKED]\"" +}, +{ +"event":"CVv: [CVV-MASKED]" +}, +{ +"event":"cVV:[CVV-MASKED]" +}, +{ +"event":"CvV= [CVV-MASKED]" +}, +{ +"event":"CVV=[CVV-MASKED]" +}, +{ +"event":"Here cvv=[CVV-MASKED] is submitted" +}, +{ +"event":"'cvc': '[CVV-MASKED]'" +}, +{ +"event":"'CVC': '[CVV-MASKED]'" +}, +{ +"event":"{\"processed_data\": {\"cvv\": \"[CVV-MASKED]\"}}" +}, +{ +"event":"data {\"cvv\": \"[CVV-MASKED]\"}" +}, +{ +"event":"payment_id: abc12345" +}, +{ +"event":"payment_id= abc12345" +}, +{ +"event":"payment_id abc12345" +}, +{ +"event":"payment-id: abc12345" +}, +{ +"event":"payment-id= abc12345" +}, +{ +"event":"payment-id abc12345" +}, +{ +"event":"'payment_id': 'abc12345'" +}, +{ +"event":"'payment-id': 'abc12345'" +}, +{ +"event":"'payment_id':'abc12345'" +}, +{ +"event":"'payment-id':'abc12345'" +}, +{ +"event":"\"payment_id\": \"abc12345\"" +}, +{ +"event":"\"payment-id\": \"abc12345\"" +}, +{ +"event":"\"payment_id\":\"abc12345\"" +}, +{ +"event":"\"payment-id\":\"abc12345\"" +}, +{ +"event":"'transaction_id': 'jbzzTT577'" +}, +{ +"event":"'auth_id': '016153570198200'" +}, +{ +"event":"payment_id: 1234567890123456 done" +}, +{ +"event":"payment_id= 9876543210987654 done" +}, +{ +"event":"transaction_id: abcd1234" +}, +{ +"event":"transaction-id: abcd1234" +}, +{ +"event":"auth_id: abcd1234" +}, +{ +"event":"auth-id: abcd1234" +}, +{ +"event":"{\"processed_data\": {\"payment_id\": \"abc12345\"}}" +}, +{ +"event":"account GB33BUKB20201555555555 credited" +}, +{ +"event":"acct BE68539007547034 debited" +}, +{ +"event":"acct FR1420041010050500013M02606 debited" +}, +{ +"event":"acct BH67BMAG00001299123456 debited" +}, +{ +"event":"acct QA58DOHB00001234567890ABCDEFG debited" +}, +{ +"event":"acct GB1212345678901 debited" +}, +{ +"event":"acct GB3412345678901234 debited" +}, +{ +"event":"acct GB5612345678901234567 debited" +}, +{ +"event":"acct GB78123456789012345678 debited" +}, +{ +"event":"[PHONE-MASKED]" +}, +{ +"event":"[PHONE-MASKED]" +}, +{ +"event":"[PHONE-MASKED]" +}, +{ +"event":"[PHONE-MASKED]" +}, +{ +"event":"[PHONE-MASKED]" +}, +{ +"event":"[PHONE-MASKED]" +}, +{ +"event":"[PHONE-MASKED]" +}, +{ +"event":"call [PHONE-MASKED] now" +}, +{ +"event":"call [PHONE-MASKED] now" +}, +{ +"event":"[EMAIL-MASKED]" +}, +{ +"event":"contact [EMAIL-MASKED] now" +}, +{ +"event":"[EMAIL-MASKED]" +}, +{ +"event":"[EMAIL-MASKED]" +}, +{ +"event":"[EMAIL-MASKED]" +}, +{ +"event":"[EMAIL-MASKED]" +}, +{ +"event":"[EMAIL-MASKED]" +}, +{ +"event":"[EMAIL-MASKED]" +}, +{ +"event":"[EMAIL-MASKED]" +}, +{ +"event":"notify: [EMAIL-MASKED]" +}, +{ +"event":"'email': '[EMAIL-MASKED]'" +}, +{ +"event":"\"email\": \"[EMAIL-MASKED]\"" +}, +{ +"event":"email=[EMAIL-MASKED]" +}, +{ +"event":"{\"contact\": {\"email\": \"[EMAIL-MASKED]\"}}" +}, +{ +"event":"[JWT-MASKED]" +}, +{ +"event":"Here it's [JWT-MASKED] failed!" +}, +{ +"event":"Here it's ([JWT-MASKED]) failed!" +}, +{ +"event":"Here it's: [JWT-MASKED] failed!" +}, +{ +"event":"-[JWT-MASKED]" +}, +{ +"event":".[JWT-MASKED]" +}, +{ +"event":"[JWT-MASKED]" +}, +{ +"event":"[JWT-MASKED]" +}, +{ +"event":"[JWT-MASKED]" +}, +{ +"event":"[JWT-MASKED]" +}, +{ +"event":"token was [JWT-MASKED] and more" +}, +{ +"event":"912345678912" +}, +{ +"event":"9123456789123" +}, +{ +"event":"91234567891234" +}, +{ +"event":"912345678912345" +}, +{ +"event":"9123456789123456" +}, +{ +"event":"91234567891234567" +}, +{ +"event":"912345678912345678" +}, +{ +"event":"9123456789123456789" +}, +{ +"event":"9123 4567 8912" +}, +{ +"event":"9123 4567 8912 3" +}, +{ +"event":"9123 4567 8912 34" +}, +{ +"event":"9123 4567 8912 345" +}, +{ +"event":"9123 4567 8912 3456" +}, +{ +"event":"9123 4567 8912 34567" +}, +{ +"event":"9123 4567 8912 345678" +}, +{ +"event":"9123 4567 8912 3456789" +}, +{ +"event":"9123-4567-8912" +}, +{ +"event":"9123-4567-8912-3" +}, +{ +"event":"9123-4567-8912-34" +}, +{ +"event":"9123-4567-8912-345" +}, +{ +"event":"9123-4567-8912-3456" +}, +{ +"event":"9123-4567-8912-34567" +}, +{ +"event":"9123-4567-8912-345678" +}, +{ +"event":"9123-4567-8912-3456789" +}, +{ +"event":"112345678912" +}, +{ +"event":"1123456789123" +}, +{ +"event":"11234567891234" +}, +{ +"event":"112345678912345" +}, +{ +"event":"1123456789123456" +}, +{ +"event":"11234567891234567" +}, +{ +"event":"112345678912345678" +} +], +"no-keyset/default/prose":[ +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +"{\"session_id\": \"21a2496f8b6c193ec61b8dd52cbe8d827b4eaa49\", \"order_no\": \"ORD-2026-000123\", \"amount\": \"12.500\", \"currency_code\": \"KWD\", \"customer_email\": \"[EMAIL-MASKED]\", \"customer_phone\": \"[PHONE-MASKED]\", \"customer_first_name\": \"[NAME-MASKED]\", \"billing_address\": {\"line1\": \"[ADDRESS-MASKED]\", \"city\": \"[ADDRESS-MASKED]\", \"country\": \"[ADDRESS-MASKED]\"}, \"pg_codes\": [\"mpgs-direct\", \"cs-direct\", \"kpay\"], \"redirect_url\": \"https://merchant.example/return?ref=abc123&lang=en\", \"webhook_url\": \"https://merchant.example/hooks/ottu\", \"extra\": {\"note\": \"gift wrap\", \"items\": [{\"sku\": \"SKU-0\", \"qty\": 0}, {\"sku\": \"SKU-1\", \"qty\": 1}, {\"sku\": \"SKU-2\", \"qty\": 2}, {\"sku\": \"SKU-3\", \"qty\": 3}, {\"sku\": \"SKU-4\", \"qty\": 4}, {\"sku\": \"SKU-5\", \"qty\": 5}, {\"sku\": \"SKU-6\", \"qty\": 6}, {\"sku\": \"SKU-7\", \"qty\": 7}, {\"sku\": \"SKU-8\", \"qty\": 8}, {\"sku\": \"SKU-9\", \"qty\": 9}]}, \"card_acceptance_criteria\": {\"min_expiry_time\": 30}, \"three_ds\": {\"directory_server_transaction_id\": \"3d85c5d3-8412-4f1d-9e26-4a0f00123456\"}}", +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null +], +"no-keyset/default/records":[ +[ +"user %s paid %s KWD for %s", +"('bob', '12.500', 'ORD-1')", +"user bob paid 12.500 KWD for ORD-1" +], +[ +"cvv=[CVV-MASKED]", +"()", +"cvv=[CVV-MASKED]" +], +[ +"card %s %s", +"('4111111111111111', '123')", +"card 4111111111111111 123" +], +[ +"password=[SECRET-MASKED]", +"()", +"password=[SECRET-MASKED]" +], +[ +"paid 10 KWD by [PHONE-MASKED]", +"()", +"paid 10 KWD by [PHONE-MASKED]" +], +[ +"sent to %s", +"('[EMAIL-MASKED]',)", +"sent to [EMAIL-MASKED]" +], +[ +"apple pay token %s", +"('[SAD-MASKED]',)", +"apple pay token [SAD-MASKED]" +], +[ +"retried with Bearer [SECRET-MASKED]", +"()", +"retried with Bearer [SECRET-MASKED]" +], +[ +"%s", +"{'card_number': '411111******1111', 'cvv': '[CVV-MASKED]'}", +"{'card_number': '411111******1111', 'cvv': '[CVV-MASKED]'}" +] +], +"no-keyset/default/text":[ +"[PEM-KEY-MASKED]", +"[PEM-KEY-MASKED]", +"[PEM-KEY-MASKED]", +"[PEM-KEY-MASKED]", +"[PEM-KEY-MASKED]", +"'token': '[SECRET-MASKED]'", +"'token':'[SECRET-MASKED]'", +"\"token\": \"[SECRET-MASKED]\"", +"\"token\":\"[SECRET-MASKED]\"", +"token: [SECRET-MASKED]", +"token:[SECRET-MASKED]", +"token= [SECRET-MASKED]", +"token=[SECRET-MASKED]", +"token=[SECRET-MASKED]", +"token=[SECRET-MASKED]", +"token: [SECRET-MASKED]", +"token: [SECRET-MASKED]", +"token:[SECRET-MASKED]", +"token=[SECRET-MASKED]", +"token:[SECRET-MASKED]", +"token=[SECRET-MASKED]", +"token [SECRET-MASKED]", +"token [SECRET-MASKED]", +"token [SECRET-MASKED]", +"message token=[SECRET-MASKED]", +"anyword_token [SECRET-MASKED]", +"any_word_token [SECRET-MASKED]", +"any-word_token [SECRET-MASKED]", +"any-word-token [SECRET-MASKED]", +"any_1-word_2-token [SECRET-MASKED]", +"secret [SECRET-MASKED]", +"any_1-word_2-secret [SECRET-MASKED]", +"password [SECRET-MASKED]", +"any_1-word_2-password [SECRET-MASKED]", +"passwd [SECRET-MASKED]", +"any_1-word_2-passwd [SECRET-MASKED]", +"bearer [SECRET-MASKED]", +"basic [SECRET-MASKED]", +"digest [SECRET-MASKED]", +"credential [SECRET-MASKED]", +"credentials [SECRET-MASKED]", +"authorization [SECRET-MASKED]", +"authorization_header [SECRET-MASKED]", +"authorization-header [SECRET-MASKED]", +"authorisation [SECRET-MASKED]", +"authorisation_header [SECRET-MASKED]", +"authorisation-header [SECRET-MASKED]", +"apikey [SECRET-MASKED]", +"api_key [SECRET-MASKED]", +"[SECRET-MASKED]", +"secret-key [SECRET-MASKED]", +"private-key [SECRET-MASKED]", +"public-key [SECRET-MASKED]", +"encryption-key [SECRET-MASKED]", +"decryption-key [SECRET-MASKED]", +"signing-key [SECRET-MASKED]", +"access-key [SECRET-MASKED]", +"master-key [SECRET-MASKED]", +"root-key [SECRET-MASKED]", +"session-key [SECRET-MASKED]", +"secret_key=[SECRET-MASKED]", +"'Authorization': '[SECRET-MASKED]'", +"\"Authorization\": \"[SECRET-MASKED]\"", +"Authorization: [SECRET-MASKED]", +"Authorization= [SECRET-MASKED]", +"Authorization Bearer [SECRET-MASKED]", +"{\"Authorization\": \"[SECRET-MASKED]\"}", +"Here is {\"Authorization\": \"[SECRET-MASKED]\"}", +"{\"Authorization\": \"[SECRET-MASKED]\"}", +"Authorization: [SECRET-MASKED]", +"{\"Authorisation\": \"[SECRET-MASKED]\"}", +"Authentication failed with key '[SECRET-MASKED]'", +"access_token=[SECRET-MASKED]", +"refresh_token=[SECRET-MASKED]", +"client_secret=[SECRET-MASKED]", +"private_key=[SECRET-MASKED]", +"secret_key=[SECRET-MASKED]", +"basic= [SECRET-MASKED]", +"[SECRET-MASKED]", +"Basic [SECRET-MASKED]", +null, +null, +null, +"body {\"session_key\": \"[SECRET-MASKED]\"}", +"body {'api_key': '[SECRET-MASKED]'}", +"{\"result\": \"SUCCESS\", \"session\": {\"aes256Key\": \"[SECRET-MASKED]\", \"id\": \"SESS", +"aes256Key=[SECRET-MASKED]", +"hmacSha256Key=[SECRET-MASKED]", +"privateKeyPem=[SECRET-MASKED]", +null, +"'cvv': '[CVV-MASKED]'", +"'Cvv':'[CVV-MASKED]'", +"\"cVv\": \"[CVV-MASKED]\"", +"\"cvV\":\"[CVV-MASKED]\"", +"CVv: [CVV-MASKED]", +"cVV:[CVV-MASKED]", +"CvV= [CVV-MASKED]", +"CVV=[CVV-MASKED]", +"Here cvv=[CVV-MASKED] is submitted", +"'cvc': '[CVV-MASKED]'", +"'CVC': '[CVV-MASKED]'", +"{\"processed_data\": {\"cvv\": \"[CVV-MASKED]\"}}", +"data {\"cvv\": \"[CVV-MASKED]\"}", +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +"[PHONE-MASKED]", +"[PHONE-MASKED]", +"[PHONE-MASKED]", +"[PHONE-MASKED]", +"[PHONE-MASKED]", +"[PHONE-MASKED]", +"[PHONE-MASKED]", +"call [PHONE-MASKED] now", +"call [PHONE-MASKED] now", +"[EMAIL-MASKED]", +"contact [EMAIL-MASKED] now", +"[EMAIL-MASKED]", +"[EMAIL-MASKED]", +"[EMAIL-MASKED]", +"[EMAIL-MASKED]", +"[EMAIL-MASKED]", +"[EMAIL-MASKED]", +"[EMAIL-MASKED]", +"notify: [EMAIL-MASKED]", +"'email': '[EMAIL-MASKED]'", +"\"email\": \"[EMAIL-MASKED]\"", +"email=[EMAIL-MASKED]", +"{\"contact\": {\"email\": \"[EMAIL-MASKED]\"}}", +"[JWT-MASKED]", +"Here it's [JWT-MASKED] failed!", +"Here it's ([JWT-MASKED]) failed!", +"Here it's: [JWT-MASKED] failed!", +"-[JWT-MASKED]", +".[JWT-MASKED]", +"[JWT-MASKED]", +"[JWT-MASKED]", +"[JWT-MASKED]", +"[JWT-MASKED]", +"token was [JWT-MASKED] and more", +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +"", +"error: client_secret=\"[SECRET-MASKED]\" rejected", +"", +"x", +"{\"password\": \"[SECRET-MASKED]}", +"{\"id\": 1, \"password\": \"[SECRET-MASKED] x", +"password=[SECRET-MASKED], status=ok", +"password=[SECRET-MASKED];", +"password=[SECRET-MASKED]'", +"password=[SECRET-MASKED]\",4111111111111111&x=1", +"password=[SECRET-MASKED]\":cd&x=1", +"password=\"[SECRET-MASKED]\"&x=1", +"password=[SECRET-MASKED]&x=1", +"{\"xml\": \"\"}", +"{\"note\": \"password=\\\"[SECRET-MASKED]\\\"&x=1\"}", +"{\"note\": \"password=[SECRET-MASKED]&x=1\"}", +"{\"password\": \"[SECRET-MASKED]\"}", +"{\"url\": \"https://x?secret=[SECRET-MASKED]\"}", +"{\"password\": \"[SECRET-MASKED]\"}", +"could not connect postgresql://[SECRET-MASKED]:[SECRET-MASKED]@localhost:5432/db", +"amqp://[SECRET-MASKED]:[SECRET-MASKED]@10.0.0.5//", +"GET https://[SECRET-MASKED]:@api.host/ failed", +"redis://:[SECRET-MASKED]@cache:6379/0 refused", +"{\"dsn\": \"mongodb+srv://[SECRET-MASKED]:[SECRET-MASKED]@cluster0.example.net/app\"}", +"fetch https://[SECRET-MASKED]:[SECRET-MASKED]@api.host.example/v1/pay", +"amqp://[SECRET-MASKED]:[SECRET-MASKED]@mq:5672/", +"amqp://[SECRET-MASKED]:[SECRET-MASKED]@mq:5672/", +"sent {\"password\": \"[SECRET-MASKED]\"} to the gateway", +"login with password=[SECRET-MASKED] failed", +"retried with Bearer [SECRET-MASKED]", +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +"card 411111******1111 cvv [CVV-MASKED]", +"cvv=[CVV-MASKED] for 411111******1111", +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +"TRANPORTAL123[SECRET-MASKED]110.000TRK1{\"paymentData\":\"[SAD-MASKED]\"}", +"TRANPORTAL123[SECRET-MASKED]810.000202612345TRK1TrackID", +"TRANPORTAL123[SECRET-MASKED]25.000202612345TRK1TrackID", +"{\"session_id\": \"21a2496f8b6c193ec61b8dd52cbe8d827b4eaa49\", \"order_no\": \"ORD-2026-000123\", \"amount\": \"12.500\", \"currency_code\": \"KWD\", \"customer_email\": \"[EMAIL-MASKED]\", \"customer_phone\": \"[PHONE-MASKED]\", \"customer_first_name\": \"Jane\", \"billing_address\": {\"line1\": \"Block 1, Street 2\", \"city\": \"Kuwait City\", \"country\": \"KW\"}, \"pg_codes\": [\"mpgs-direct\", \"cs-direct\", \"kpay\"], \"redirect_url\": \"https://merchant.example/return?ref=abc123&lang=en\", \"webhook_url\": \"https://merchant.example/hooks/ottu\", \"extra\": {\"note\": \"gift wrap\", \"items\": [{\"sku\": \"SKU-0\", \"qty\": 0}, {\"sku\": \"SKU-1\", \"qty\": 1}, {\"sku\": \"SKU-2\", \"qty\": 2}, {\"sku\": \"SKU-3\", \"qty\": 3}, {\"sku\": \"SKU-4\", \"qty\": 4}, {\"sku\": \"SKU-5\", \"qty\": 5}, {\"sku\": \"SKU-6\", \"qty\": 6}, {\"sku\": \"SKU-7\", \"qty\": 7}, {\"sku\": \"SKU-8\", \"qty\": 8}, {\"sku\": \"SKU-9\", \"qty\": 9}]}, \"card_acceptance_criteria\": {\"min_expiry_time\": 30}, \"three_ds\": {\"directory_server_transaction_id\": \"3d85c5d3-8412-4f1d-9e26-4a0f00123456\"}}", +null, +"[SAD-MASKED][EMAIL-MASKED]]", +"https://[SECRET-MASKED]@h.example/", +"[the cvv is [PHONE-MASKED]", +"123-45-6789350 mshttps://[SECRET-MASKED]@h.example/[SAD-MASKED],", +"Bearer [SECRET-MASKED]\": \"", +null, +"Authorization: [SECRET-MASKED][)", +null, +"12/27abc123\"{{,[EMAIL-MASKED] cvv is )", +"([JWT-MASKED]+96550000000token=[EMAIL-MASKED],{\"[SAD-MASKED]\"12/27", +"}cvv https://[SECRET-MASKED]:[SECRET-MASKED]@h/"", +null, +"https://[SECRET-MASKED]:[SECRET-MASKED]@h/abc123cvv=:cvv |", +null, +"[EMAIL-MASKED]\\", +null, +"350 ms\\token=[SECRET-MASKED]}password=}{Authorization: \"[SECRET-MASKED]\": \"", +"1234Bearer \"https://[SECRET-MASKED]:[SECRET-MASKED]@h/[SAD-MASKED]\"token\": \"", +"[CVV-MASKED](", +"200\"[SAD-MASKED]\"12/27)jane@example.com123411111111111111112/27200", +null, +null, +"123[cvv=,[PHONE-MASKED]", +",(\"[SAD-MASKED]\"Authorization: [SECRET-MASKED]>", +"GB33BUKB20201555555555,\"token\": \"[SECRET-MASKED]>", +"\"[SAD-MASKED]\"https://[SECRET-MASKED]@h.example/ (350 msabc123\"350 mspassword=", +"\\https://[SECRET-MASKED]@h.example/", +",["the cvv is [JWT-MASKED]", +"https://[SECRET-MASKED]:[SECRET-MASKED]@h/411111******1111\"token\": \"}"the cvv is Bearer password=", +"(cvv [SAD-MASKED]cvv=[EMAIL-MASKED]:[JWT-MASKED]", +"[PHONE-MASKED]cvv=350 ms", +null, +"{password=[SECRET-MASKED] cvv is GB33BUKB20201555555555{abc123123-45-6789123-45-6789", +"[SAD-MASKED]password='}", +null, +null, +"][EMAIL-MASKED](ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\"https://[SECRET-MASKED]:[SECRET-MASKED]@h/]", +"Authorization: [SECRET-MASKED]", +"\"token\": \"[SECRET-MASKED]&password=[PEM-KEY-MASKED](Authorization: [SECRET-MASKED]", +"123|(4111111111111111cvv [CVV-MASKED]", +"|123-45-6789}cvv=the cvv is [SAD-MASKED] 200", +"|https://[SECRET-MASKED]:[SECRET-MASKED]@h/\"[SAD-MASKED]\"{", +"{\"[SAD-MASKED]\"abc123", +null, +":\"[SAD-MASKED]\"cvv=https://[SECRET-MASKED]@h.example/payment_id=abc12345the cvv is [JWT-MASKED]", +null, +null, +null, +"|'(:)[SAD-MASKED]{[ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", +":\"[SAD-MASKED]\"}123412/27200GB33BUKB20201555555555'", +null, +"\"token\": \"[SECRET-MASKED][SAD-MASKED]4111111111111111}123-45-6789", +"cvv [CVV-MASKED]\"[SAD-MASKED]\"|12/27{GB33BUKB20201555555555350 ms", +"+96550000000token=Authorization: [SECRET-MASKED] cvv is \"token\": \"", +null, +null, +"Bearer 350 ms123-45-6789:[EMAIL-MASKED] ", +"GB33BUKB20201555555555(https://[SECRET-MASKED]@h.example/:]ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA123412/27the cvv is ", +"cvv token=[SECRET-MASKED]", +null, +null, +"123411111******1111GB33BUKB20201555555555abc123123\"[SAD-MASKED]\"cvv ", +null, +"[PHONE-MASKED]", +"https://[SECRET-MASKED]@h.example/123the cvv is }", +null, +"'s3cr3thttps://[SECRET-MASKED]:[SECRET-MASKED]@h/[password=[SECRET-MASKED]:'", +"password=[SECRET-MASKED] cvv='jane@example.comGB33BUKB20201555555555", +null, +null, +"]411111******1111123-45-6789cvv=4111111111111111[SAD-MASKED])", +null, +null, +"|[JWT-MASKED]", +"\\Bearer [SECRET-MASKED]\": \"]", +null, +null, +null, +null, +""cvv [CVV-MASKED]\"[SAD-MASKED]\"\"[SAD-MASKED]\"\"Authorization: ", +"[PEM-KEY-MASKED]abc123,200350 ms\"token\": \"[", +"[Bearer [SECRET-MASKED]", +"\\123-45-6789https://u:p@h/https://[SECRET-MASKED]:[SECRET-MASKED]@h/", +null, +"&350 ms&',Authorization: [SECRET-MASKED]", +"&123-45-6789Bearer https://[SECRET-MASKED]@h.example/]200jane@example.comGB33BUKB20201555555555abc123", +")[EMAIL-MASKED] cvv is ", +"}[JWT-MASKED]", +null, +"\"[SAD-MASKED]\"411111******1111'[the cvv is payment_id=abc12345token=token=", +null, +null, +null, +"jane@example.com350 ms\"[SAD-MASKED]\"\"[JWT-MASKED]&", +" [EMAIL-MASKED]+96550000000cvv=&]", +"\"[PHONE-MASKED][SAD-MASKED]|[SAD-MASKED] (", +null, +"\"token=[SECRET-MASKED]>[PEM-KEY-MASKED]", +"123-45-678912/27eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.c2lnptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[SAD-MASKED]\"token\": \"[SECRET-MASKED]", +null, +"+96550000000cvv [JWT-MASKED]\\https://[SECRET-MASKED]@h.example/", +null, +"[SAD-MASKED]", +null, +"Authorization: [SECRET-MASKED] ms", +"}|[JWT-MASKED]", +"]350 [EMAIL-MASKED]=s3cr3teyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.c2ln", +"GB33BUKB20201555555555|[EMAIL-MASKED]@example.com123+96550000000+96550000000[", +" 123-45-6789&abc123[EMAIL-MASKED]:", +"(token=[SECRET-MASKED]>(password=the cvv is GB33BUKB20201555555555", +null, +"200https://[SECRET-MASKED]:[SECRET-MASKED]@h/ the cvv is https://[SECRET-MASKED]:[SECRET-MASKED]@h/[PEM-KEY-MASKED])password=", +"Bearer [SECRET-MASKED][SAD-MASKED]", +null, +"[SAD-MASKED]|411111******1111{[SECRET-MASKED]payment_id=abc12345411111******1111token=", +"12/27\"[SAD-MASKED]\"350 ms123-45-6789[", +"\"[SAD-MASKED]\"123-45-6789the cvv is ]", +null, +"'the cvv is https://[SECRET-MASKED]@h.example/"cvv=", +"[JWT-MASKED]=)", +null, +null, +null, +"token=\"[SAD-MASKED]\"token=", +"': [EMAIL-MASKED]{https://[SECRET-MASKED]:[SECRET-MASKED]@h/", +"200&:\"[SAD-MASKED]\""411111******1111the cvv is \"[SAD-MASKED]\"", +null, +"https://[SECRET-MASKED]@h.example/[cvv s3cr3t\"", +":(|the cvv is password=[SECRET-MASKED]>", +null, +null, +"[PEM-KEY-MASKED]'(\\https://[SECRET-MASKED]:[SECRET-MASKED]@h/&", +null, +null, +" (password=[SECRET-MASKED] ", +",[JWT-MASKED]", +"]|\"token\": \"[SECRET-MASKED],|123-45-6789payment_id=abc12345", +"{[PEM-KEY-MASKED]\"token\": \"[SECRET-MASKED] cvv=}200", +"12/27\\|https://[SECRET-MASKED]:[SECRET-MASKED]@h/GB33BUKB20201555555555\"token\": \"", +null, +null, +"Authorization: [SECRET-MASKED]", +"123 [EMAIL-MASKED]=", +"411111******1111s3cr3t1234}cvv ,}cvv=[PHONE-MASKED]", +"123[PEM-KEY-MASKED]cvv password="s3cr3thttps://[SECRET-MASKED]:[SECRET-MASKED]@h/token=[SECRET-MASKED]", +null, +":payment_id=abc12345}https://[SECRET-MASKED]:[SECRET-MASKED]@h/password=\"", +"123-45-6789https://[EMAIL-MASKED]/411111******1111", +"[EMAIL-MASKED]\"[SAD-MASKED]\" ", +"[PEM-KEY-MASKED]]\"token\": \"&abc123\\the cvv is the cvv is 123-45-6789(", +"\\[EMAIL-MASKED],cvv=,)", +null, +"'350 mshttps://[SECRET-MASKED]@h.example/'\\the cvv is ", +"https://[SECRET-MASKED]:[SECRET-MASKED]@h/password=\"[SECRET-MASKED]&123", +"\\cvv \"[SAD-MASKED]\"", +null, +"https://[SECRET-MASKED]:[SECRET-MASKED]@h/ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAuthorization: 4111111111111111123412/27Bearer [SECRET-MASKED]", +": GB33BUKB20201555555555eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.c2ln [PEM-KEY-MASKED]{ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", +"\",cvv |abc123ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA123-45-6789[PEM-KEY-MASKED]", +null, +null, +": ]cvv=https://[SECRET-MASKED]:[SECRET-MASKED]@h/", +"\"[SAD-MASKED]\")}", +"https://[SECRET-MASKED]@h.example/:the cvv is Bearer &the cvv is \"[EMAIL-MASKED]+96550000000", +null, +"password=[SECRET-MASKED]],password=", +null, +"12/27Bearer 123Authorization: password=[SECRET-MASKED][SECRET-MASKED]abc123Bearer payment_id=abc12345", +null, +null, +null, +null, +"token=[SECRET-MASKED] cvv is ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAhttps://[SECRET-MASKED]@h.example/Bearer cvv ", +null, +"\\)\"[SAD-MASKED]\"\\the cvv is [PEM-KEY-MASKED]", +null, +"|the cvv is https://[SECRET-MASKED]@h.example/ cvv ", +"[EMAIL-MASKED]'\"|\"4111111111111111abc1231234s3cr3t", +null, +"Authorization: [SECRET-MASKED]&", +"payment_id=abc12345[JWT-MASKED]\"[SAD-MASKED]\"https://[SECRET-MASKED]@h.example/Bearer \\", +null, +"Authorization: [SECRET-MASKED][SAD-MASKED]"&", +null, +"[SAD-MASKED]200s3cr3t123", +"([PEM-KEY-MASKED]]password=[SECRET-MASKED]https://[SECRET-MASKED]@h.example/", +null, +null, +"\"token\": \"[SECRET-MASKED] ms12/27350 mseyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.c2ln", +"&+96550000000]https://[SECRET-MASKED]:[SECRET-MASKED]@h/4111111111111111{\"", +"[PHONE-MASKED]\"[SECRET-MASKED]'s3cr3t\"4111111111111111GB33BUKB20201555555555s3cr3t[SECRET-MASKED]", +"https://[SECRET-MASKED]@h.example/'{\\", +null, +null, +"[EMAIL-MASKED] ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA350 msAuthorization: https://[SECRET-MASKED]:[SECRET-MASKED]@h/411111******1111", +null, +"[,Authorization: [PEM-KEY-MASKED]ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAuthorization: ", +"411111******1111[PEM-KEY-MASKED]200cvv 4111111111111111\"12/27https://u:p@h/", +"the cvv is (s3cr3t'https://[SECRET-MASKED]:[SECRET-MASKED]@h/[EMAIL-MASKED] cvv is \"token\": \"[SECRET-MASKED][SAD-MASKED]", +"\"[EMAIL-MASKED]:411111******1111", +null, +"'cvv=ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA350 mshttps://[SECRET-MASKED]:[SECRET-MASKED]@h/ https://[SECRET-MASKED]:[SECRET-MASKED]@h/123", +"{the cvv is [EMAIL-MASKED]=200"200}", +"[SAD-MASKED]\\\\", +"[PEM-KEY-MASKED] ]{s3cr3t]|[SAD-MASKED]", +null, +"411111******1111])}https://[SECRET-MASKED]:[SECRET-MASKED]@h/", +"https://[SECRET-MASKED]:[SECRET-MASKED]@h/Authorization: [SECRET-MASKED] |[SECRET-MASKED]200350 ms", +"20041111111111111114111111111111111\"ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAhttps://[EMAIL-MASKED]/ \"[SAD-MASKED]\"{", +null, +"411111******1111eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.c2ln,\"[SAD-MASKED]\"GB33BUKB20201555555555|\\", +null, +"Authorization: )'('[PEM-KEY-MASKED]the cvv is the cvv is "", +"][SAD-MASKED]&", +"Authorization: ,https://[SECRET-MASKED]@h.example/", +"1234cvv=password=\"[SECRET-MASKED]\": \"", +"12/27\"[SAD-MASKED]\"Authorization: ", +null, +"{Authorization: 200([PEM-KEY-MASKED]ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", +null, +null, +"https://[SECRET-MASKED]@h.example/cvv=[PHONE-MASKED]/27token=12/27", +"]200https://[EMAIL-MASKED]/350 ms"", +null, +null, +""https://[SECRET-MASKED]:[SECRET-MASKED]@h/|ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", +"password=[PEM-KEY-MASKED]", +null, +null, +" (1234ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAtoken=\"[SAD-MASKED]\"cvv=jane@example.com200", +"[https://[SECRET-MASKED]@h.example/ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA411111******1111\"]", +null, +"\"token\": \"[SECRET-MASKED]\" [[PEM-KEY-MASKED]token=[SECRET-MASKED]", +""123\"token\": \"[SECRET-MASKED] ", +"Bearer https://[SECRET-MASKED]@h.example/'", +"GB33BUKB20201555555555\"[SAD-MASKED]\"https://[SECRET-MASKED]@h.example/\"token\": \"[SECRET-MASKED]}[PEM-KEY-MASKED]abc123", +null, +null, +"Bearer [SECRET-MASKED]>\\]\\", +null, +"https://[SECRET-MASKED]:[SECRET-MASKED]@h/cvv cvv [SAD-MASKED]'", +"[SECRET-MASKED]&4111111111111111&https://[SECRET-MASKED]:[SECRET-MASKED]@h/\\[SECRET-MASKED]", +"}"cvv=][SECRET-MASKED][SECRET-MASKED]token=[SECRET-MASKED] 4111111111111111", +"[PEM-KEY-MASKED]", +")https://[SECRET-MASKED]:[SECRET-MASKED]@h/", +null, +"[SAD-MASKED]cvv ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[EMAIL-MASKED] cvv is }abc123", +"Authorization: [SECRET-MASKED]", +"cvv token=GB33BUKB20201555555555411111******1111https://[EMAIL-MASKED]/:[EMAIL-MASKED]", +"\"[SAD-MASKED]\"cvv cvv=[SECRET-MASKED]jane@example.com411111******1111", +"s3cr3t[SAD-MASKED]cvv=\\1234", +"Bearer (\"[SAD-MASKED]\"", +"123-45-6789{[[EMAIL-MASKED]:Bearer "", +null, +"GB33BUKB20201555555555123\"token\": \"[SECRET-MASKED][SAD-MASKED][SAD-MASKED],"]", +"https://[SECRET-MASKED]@h.example/token=[SECRET-MASKED] ms]https://[SECRET-MASKED]:[SECRET-MASKED]@h/\"|{password=", +"[&123-45-6789411111******1111+96550000000[SAD-MASKED]abc123GB33BUKB20201555555555", +"[SECRET-MASKED]4111111111111111"cvv=[JWT-MASKED][EMAIL-MASKED] cvv is ", +"[SECRET-MASKED]"Authorization: \"[SECRET-MASKED]\": \"", +null, +"cvv [CVV-MASKED][SAD-MASKED] Authorization: [SECRET-MASKED]", +null, +"\"[SAD-MASKED]\"&", +"Bearer }s3cr3tBearer [CVV-MASKED]", +"1234[SECRET-MASKED]\"[SAD-MASKED]\"ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAthe cvv is ", +"\"[SAD-MASKED]\"the cvv is 411111******1111'", +null, +null, +null, +"[EMAIL-MASKED],", +"[JWT-MASKED]\"[SECRET-MASKED]https://[SECRET-MASKED]:[SECRET-MASKED]@h/", +"|"&[PEM-KEY-MASKED]]350 ms123-45-6789", +"Bearer \"[SAD-MASKED]\"200\"[SAD-MASKED]\",:[PHONE-MASKED]}abc123", +"token=[SECRET-MASKED]", +null, +"(https://[SECRET-MASKED]@h.example/][JWT-MASKED]", +"Authorization: [CVV-MASKED]", +"(\"[SAD-MASKED]\"41111111111111111234payment_id=abc12345", +null, +")}https://[SECRET-MASKED]@h.example/[PEM-KEY-MASKED]https://[SECRET-MASKED]:[SECRET-MASKED]@h/'[EMAIL-MASKED][SECRET-MASKED]+96550000000123-45-6789", +"\"[SAD-MASKED]\"200eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.c2ln[SECRET-MASKED]cvv ", +" the cvv is 411111******1111\"}Bearer [SAD-MASKED]", +"}Bearer token=[SECRET-MASKED])", +null, +"s3cr3ttoken=ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[SAD-MASKED]cvv=,password=", +null, +"123-45-6789200)s3cr3t(Bearer [SECRET-MASKED]", +"4111111111111111123-45-6789[1234password= cvv=token=\"[SECRET-MASKED]", +"https://[SECRET-MASKED]@h.example/[EMAIL-MASKED]", +null, +"abc123https://[SECRET-MASKED]:[SECRET-MASKED]@h/Bearer &(", +"[SAD-MASKED]]\"Authorization: [SECRET-MASKED]" +], +"no-keyset/every-pack/events":[ +{ +"event":"api request received: POST /b/checkout/v1/pymt-txn/", +"http":{ +"request":{ +"body":"{\"session_id\": \"21a2496f8b6c193ec61b8dd52cbe8d827b4eaa49\", \"order_no\": \"ORD-2026-000123\", \"amount\": \"12.500\", \"currency_code\": \"KWD\", \"customer_email\": \"[EMAIL-MASKED]\", \"customer_phone\": \"[PHONE-MASKED]\", \"customer_first_name\": \"[NAME-MASKED]\", \"billing_address\": {\"line1\": \"[ADDRESS-MASKED]\", \"city\": \"[ADDRESS-MASKED]\", \"country\": \"[ADDRESS-MASKED]\"}, \"pg_codes\": [\"mpgs-direct\", \"cs-direct\", \"kpay\"], \"redirect_url\": \"https://merchant.example/return?ref=abc123&lang=en\", \"webhook_url\": \"https://merchant.example/hooks/ottu\", \"extra\": {\"note\": \"gift wrap\", \"items\": [{\"sku\": \"SKU-0\", \"qty\": 0}, {\"sku\": \"SKU-1\", \"qty\": 1}, {\"sku\": \"SKU-2\", \"qty\": 2}, {\"sku\": \"SKU-3\", \"qty\": 3}, {\"sku\": \"SKU-4\", \"qty\": 4}, {\"sku\": \"SKU-5\", \"qty\": 5}, {\"sku\": \"SKU-6\", \"qty\": 6}, {\"sku\": \"SKU-7\", \"qty\": 7}, {\"sku\": \"SKU-8\", \"qty\": 8}, {\"sku\": \"SKU-9\", \"qty\": 9}]}, \"card_acceptance_criteria\": {\"min_expiry_time\": 30}, \"three_ds\": {\"directory_server_transaction_id\": \"[PAYMENT-ID-MASKED]\"}}", +"headers":{ +"authorization":"[SECRET-MASKED]", +"user-agent":"Mozilla/5.0" +} +} +}, +"payload":{ +"amount":"12.500", +"billing_address":{ +"city":"[ADDRESS-MASKED]", +"country":"[ADDRESS-MASKED]", +"line1":"[ADDRESS-MASKED]" +}, +"card_acceptance_criteria":{ +"min_expiry_time":30 +}, +"currency_code":"KWD", +"customer_email":"[EMAIL-MASKED]", +"customer_first_name":"[NAME-MASKED]", +"customer_phone":"[PHONE-MASKED]", +"extra":{ +"items":[ +{ +"qty":0, +"sku":"SKU-0" +}, +{ +"qty":1, +"sku":"SKU-1" +}, +{ +"qty":2, +"sku":"SKU-2" +}, +{ +"qty":3, +"sku":"SKU-3" +}, +{ +"qty":4, +"sku":"SKU-4" +}, +{ +"qty":5, +"sku":"SKU-5" +}, +{ +"qty":6, +"sku":"SKU-6" +}, +{ +"qty":7, +"sku":"SKU-7" +}, +{ +"qty":8, +"sku":"SKU-8" +}, +{ +"qty":9, +"sku":"SKU-9" +} +], +"note":"gift wrap" +}, +"order_no":"ORD-2026-000123", +"pg_codes":[ +"mpgs-direct", +"cs-direct", +"kpay" +], +"redirect_url":"https://merchant.example/return?ref=abc123&lang=en", +"session_id":"21a2496f8b6c193ec61b8dd52cbe8d827b4eaa49", +"three_ds":{ +"directory_server_transaction_id":"[PAYMENT-ID-MASKED]" +}, +"webhook_url":"https://merchant.example/hooks/ottu" +}, +"url":{ +"full":"https://jade.ottu.dev/b/checkout/v1/pymt-txn/?utm=1" +} +}, +{ +"event":"pg.response_received from apitest.cybersource.com", +"http":{ +"request":{ +"method":"POST" +}, +"response":{ +"body":"{\"id\": \"pay_9f8e7d6c5b4a\", \"status\": \"AUTHORIZED\", \"amount\": \"100.000\", \"currency\": \"KWD\", \"processorInformation\": {\"approvalCode\": \"831000\", \"responseCode\": \"100\", \"transactionId\": \"726650******4567\"}, \"orderInformation\": {\"amountDetails\": {\"authorizedAmount\": \"100.000\", \"currency\": \"KWD\"}}, \"_links\": {\"self\": {\"href\": \"/pts/v2/payments/7266500001234567\"}}}", +"status_code":201 +} +}, +"labels":{ +"direction":"outbound", +"namespace":"cybersource", +"operation":"payment", +"pg_result":"AUTHORIZED" +}, +"merchant_id":"jade.ottu.dev", +"payment":{ +"pg_code":"cs-direct", +"reference":"jade-oTTE45" +}, +"service":{ +"target":{ +"name":"cybersource" +} +}, +"session_id":"b1e2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0", +"url":{ +"full":"https://apitest.cybersource.com/pts/v2/payments/" +} +}, +{ +"event":"pg.response_received from apitest.cybersource.com", +"http":{ +"request":{ +"method":"POST" +}, +"response":{ +"body":"{\"access_token\": \"[SECRET-MASKED]\", \"id\": \"pay_9f8e7d6c5b4a\", \"status\": \"AUTHORIZED\", \"amount\": \"100.000\", \"currency\": \"KWD\", \"processorInformation\": {\"approvalCode\": \"831000\", \"responseCode\": \"100\", \"transactionId\": \"726650******4567\"}, \"orderInformation\": {\"amountDetails\": {\"authorizedAmount\": \"100.000\", \"currency\": \"KWD\"}}, \"_links\": {\"self\": {\"href\": \"/pts/v2/payments/7266500001234567\"}}}", +"status_code":201 +} +}, +"labels":{ +"direction":"outbound", +"namespace":"cybersource", +"operation":"payment", +"pg_result":"AUTHORIZED" +}, +"merchant_id":"jade.ottu.dev", +"payment":{ +"pg_code":"cs-direct", +"reference":"jade-oTTE45" +}, +"service":{ +"target":{ +"name":"cybersource" +} +}, +"session_id":"b1e2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0", +"url":{ +"full":"https://apitest.cybersource.com/pts/v2/payments/" +} +}, +{ +"event":"pg.request_sent to MPGS", +"message_text":"card 411111******1111 cvv [CVV-MASKED] for [EMAIL-MASKED]", +"payload":{ +"customer":{ +"email":"[EMAIL-MASKED]", +"firstName":"[NAME-MASKED]" +}, +"order":{ +"amount":20, +"reference":"deltabRKJ5X_0" +}, +"sourceOfFunds":{ +"provided":{ +"card":{ +"expiry":{ +"month":"01", +"year":"27" +}, +"number":"411111******1111", +"securityCode":"[CVV-MASKED]" +} +} +} +}, +"session_id":"b1e2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0" +}, +{ +"event":"create payment", +"token":"[SECRET-MASKED]" +}, +{ +"cvv":"[CVV-MASKED]" +}, +{ +"cvv":"[CVV-MASKED]" +}, +{ +"session_key":"[SECRET-MASKED]" +}, +{ +"access_token":null +}, +{ +"card":null, +"customer_name":null, +"cvv":null, +"expiry":null, +"public_key":null +}, +{ +"Cvv":"[CVV-MASKED]" +}, +{ +"securityCode":"[CVV-MASKED]" +}, +{ +"data":{ +"token":{ +"more":"[SECRET-MASKED]", +"nested":"[SECRET-MASKED]" +} +} +}, +{ +"payload":{ +"note":"[SAD-MASKED]", +"paymentData":"[SAD-MASKED]" +} +}, +{ +"http":{ +"request":{ +"body":{ +"content":"TRANPORTAL123[SECRET-MASKED]110.000TRK1{\"paymentData\":\"[SAD-MASKED]\"}" +} +} +} +}, +{ +"http":{ +"request":{ +"body":{ +"content":"TRANPORTAL123[SECRET-MASKED]810.000202612345TRK1TrackID" +} +} +} +}, +{ +"http":{ +"request":{ +"body":{ +"content":"TRANPORTAL123[SECRET-MASKED]25.000202612345TRK1TrackID" +} +} +} +}, +{ +"event":"[PEM-KEY-MASKED]" +}, +{ +"event":"[PEM-KEY-MASKED]" +}, +{ +"event":"[PEM-KEY-MASKED]" +}, +{ +"event":"[PEM-KEY-MASKED]" +}, +{ +"event":"[PEM-KEY-MASKED]" +}, +{ +"event":"'token': '[SECRET-MASKED]'" +}, +{ +"event":"'token':'[SECRET-MASKED]'" +}, +{ +"event":"\"token\": \"[SECRET-MASKED]\"" +}, +{ +"event":"\"token\":\"[SECRET-MASKED]\"" +}, +{ +"event":"token: [SECRET-MASKED]" +}, +{ +"event":"token:[SECRET-MASKED]" +}, +{ +"event":"token= [SECRET-MASKED]" +}, +{ +"event":"token=[SECRET-MASKED]" +}, +{ +"event":"token=[SECRET-MASKED]" +}, +{ +"event":"token=[SECRET-MASKED]" +}, +{ +"event":"token: [SECRET-MASKED]" +}, +{ +"event":"token: [SECRET-MASKED]" +}, +{ +"event":"token:[SECRET-MASKED]" +}, +{ +"event":"token=[SECRET-MASKED]" +}, +{ +"event":"token:[SECRET-MASKED]" +}, +{ +"event":"token=[SECRET-MASKED]" +}, +{ +"event":"token [SECRET-MASKED]" +}, +{ +"event":"token [SECRET-MASKED]" +}, +{ +"event":"token [SECRET-MASKED]" +}, +{ +"event":"message token=[SECRET-MASKED]" +}, +{ +"event":"anyword_token [SECRET-MASKED]" +}, +{ +"event":"any_word_token [SECRET-MASKED]" +}, +{ +"event":"any-word_token [SECRET-MASKED]" +}, +{ +"event":"any-word-token [SECRET-MASKED]" +}, +{ +"event":"any_1-word_2-token [SECRET-MASKED]" +}, +{ +"event":"secret [SECRET-MASKED]" +}, +{ +"event":"any_1-word_2-secret [SECRET-MASKED]" +}, +{ +"event":"password [SECRET-MASKED]" +}, +{ +"event":"any_1-word_2-password [SECRET-MASKED]" +}, +{ +"event":"passwd [SECRET-MASKED]" +}, +{ +"event":"any_1-word_2-passwd [SECRET-MASKED]" +}, +{ +"event":"bearer [SECRET-MASKED]" +}, +{ +"event":"basic [SECRET-MASKED]" +}, +{ +"event":"digest [SECRET-MASKED]" +}, +{ +"event":"credential [SECRET-MASKED]" +}, +{ +"event":"credentials [SECRET-MASKED]" +}, +{ +"event":"authorization [SECRET-MASKED]" +}, +{ +"event":"authorization_header [SECRET-MASKED]" +}, +{ +"event":"authorization-header [SECRET-MASKED]" +}, +{ +"event":"authorisation [SECRET-MASKED]" +}, +{ +"event":"authorisation_header [SECRET-MASKED]" +}, +{ +"event":"authorisation-header [SECRET-MASKED]" +}, +{ +"event":"apikey [SECRET-MASKED]" +}, +{ +"event":"api_key [SECRET-MASKED]" +}, +{ +"event":"[SECRET-MASKED]" +}, +{ +"event":"secret-key [SECRET-MASKED]" +}, +{ +"event":"private-key [SECRET-MASKED]" +}, +{ +"event":"public-key [SECRET-MASKED]" +}, +{ +"event":"encryption-key [SECRET-MASKED]" +}, +{ +"event":"decryption-key [SECRET-MASKED]" +}, +{ +"event":"signing-key [SECRET-MASKED]" +}, +{ +"event":"access-key [SECRET-MASKED]" +}, +{ +"event":"master-key [SECRET-MASKED]" +}, +{ +"event":"root-key [SECRET-MASKED]" +}, +{ +"event":"session-key [SECRET-MASKED]" +}, +{ +"event":"secret_key=[SECRET-MASKED]" +}, +{ +"event":"'Authorization': '[SECRET-MASKED]'" +}, +{ +"event":"\"Authorization\": \"[SECRET-MASKED]\"" +}, +{ +"event":"Authorization: [SECRET-MASKED]" +}, +{ +"event":"Authorization= [SECRET-MASKED]" +}, +{ +"event":"Authorization Bearer [SECRET-MASKED]" +}, +{ +"event":"{\"Authorization\": \"[SECRET-MASKED]\"}" +}, +{ +"event":"Here is {\"Authorization\": \"[SECRET-MASKED]\"}" +}, +{ +"event":"{\"Authorization\": \"[SECRET-MASKED]\"}" +}, +{ +"event":"Authorization: [SECRET-MASKED]" +}, +{ +"event":"{\"Authorisation\": \"[SECRET-MASKED]\"}" +}, +{ +"event":"Authentication failed with key '[SECRET-MASKED]'" +}, +{ +"event":"access_token=[SECRET-MASKED]" +}, +{ +"event":"refresh_token=[SECRET-MASKED]" +}, +{ +"event":"client_secret=[SECRET-MASKED]" +}, +{ +"event":"private_key=[SECRET-MASKED]" +}, +{ +"event":"secret_key=[SECRET-MASKED]" +}, +{ +"event":"basic= [SECRET-MASKED]" +}, +{ +"event":"[SECRET-MASKED]" +}, +{ +"event":"Basic [SECRET-MASKED]" +}, +{ +"event":"body {\"public_key\": null, \"a\": 1}" +}, +{ +"event":"body {\"session_key\": true}" +}, +{ +"event":"body {'public_key': None}" +}, +{ +"event":"body {\"session_key\": \"[SECRET-MASKED]\"}" +}, +{ +"event":"body {'api_key': '[SECRET-MASKED]'}" +}, +{ +"event":"{\"result\": \"SUCCESS\", \"session\": {\"aes256Key\": \"[SECRET-MASKED]\", \"id\": \"SESS" +}, +{ +"event":"aes256Key=[SECRET-MASKED]" +}, +{ +"event":"hmacSha256Key=[SECRET-MASKED]" +}, +{ +"event":"privateKeyPem=[SECRET-MASKED]" +}, +{ +"event":"codes_key=abcd1234efgh5678" +}, +{ +"event":"'cvv': '[CVV-MASKED]'" +}, +{ +"event":"'Cvv':'[CVV-MASKED]'" +}, +{ +"event":"\"cVv\": \"[CVV-MASKED]\"" +}, +{ +"event":"\"cvV\":\"[CVV-MASKED]\"" +}, +{ +"event":"CVv: [CVV-MASKED]" +}, +{ +"event":"cVV:[CVV-MASKED]" +}, +{ +"event":"CvV= [CVV-MASKED]" +}, +{ +"event":"CVV=[CVV-MASKED]" +}, +{ +"event":"Here cvv=[CVV-MASKED] is submitted" +}, +{ +"event":"'cvc': '[CVV-MASKED]'" +}, +{ +"event":"'CVC': '[CVV-MASKED]'" +}, +{ +"event":"{\"processed_data\": {\"cvv\": \"[CVV-MASKED]\"}}" +}, +{ +"event":"data {\"cvv\": \"[CVV-MASKED]\"}" +}, +{ +"event":"payment_id: [PAYMENT-ID-MASKED]" +}, +{ +"event":"payment_id= [PAYMENT-ID-MASKED]" +}, +{ +"event":"payment_id [PAYMENT-ID-MASKED]" +}, +{ +"event":"payment-id: [PAYMENT-ID-MASKED]" +}, +{ +"event":"payment-id= [PAYMENT-ID-MASKED]" +}, +{ +"event":"payment-id [PAYMENT-ID-MASKED]" +}, +{ +"event":"'payment_id': '[PAYMENT-ID-MASKED]'" +}, +{ +"event":"'payment-id': '[PAYMENT-ID-MASKED]'" +}, +{ +"event":"'payment_id':'[PAYMENT-ID-MASKED]'" +}, +{ +"event":"'payment-id':'[PAYMENT-ID-MASKED]'" +}, +{ +"event":"\"payment_id\": \"[PAYMENT-ID-MASKED]\"" +}, +{ +"event":"\"payment-id\": \"[PAYMENT-ID-MASKED]\"" +}, +{ +"event":"\"payment_id\":\"[PAYMENT-ID-MASKED]\"" +}, +{ +"event":"\"payment-id\":\"[PAYMENT-ID-MASKED]\"" +}, +{ +"event":"'transaction_id': '[PAYMENT-ID-MASKED]'" +}, +{ +"event":"'auth_id': '[PAYMENT-ID-MASKED]'" +}, +{ +"event":"payment_id: [PAYMENT-ID-MASKED] done" +}, +{ +"event":"payment_id= [PAYMENT-ID-MASKED] done" +}, +{ +"event":"transaction_id: [PAYMENT-ID-MASKED]" +}, +{ +"event":"transaction-id: [PAYMENT-ID-MASKED]" +}, +{ +"event":"auth_id: [PAYMENT-ID-MASKED]" +}, +{ +"event":"auth-id: [PAYMENT-ID-MASKED]" +}, +{ +"event":"{\"processed_data\": {\"payment_id\": \"[PAYMENT-ID-MASKED]\"}}" +}, +{ +"event":"account [IBAN-MASKED] credited" +}, +{ +"event":"acct [IBAN-MASKED] debited" +}, +{ +"event":"acct [IBAN-MASKED] debited" +}, +{ +"event":"acct [IBAN-MASKED] debited" +}, +{ +"event":"acct [IBAN-MASKED] debited" +}, +{ +"event":"acct [IBAN-MASKED] debited" +}, +{ +"event":"acct [IBAN-MASKED] debited" +}, +{ +"event":"acct [IBAN-MASKED] debited" +}, +{ +"event":"acct [IBAN-MASKED] debited" +}, +{ +"event":"[PHONE-MASKED]" +}, +{ +"event":"[PHONE-MASKED]" +}, +{ +"event":"[PHONE-MASKED]" +}, +{ +"event":"[PHONE-MASKED]" +}, +{ +"event":"[PHONE-MASKED]" +}, +{ +"event":"[PHONE-MASKED]" +}, +{ +"event":"[PHONE-MASKED]" +}, +{ +"event":"call [PHONE-MASKED] now" +}, +{ +"event":"call [PHONE-MASKED] now" +}, +{ +"event":"[EMAIL-MASKED]" +}, +{ +"event":"contact [EMAIL-MASKED] now" +}, +{ +"event":"[EMAIL-MASKED]" +}, +{ +"event":"[EMAIL-MASKED]" +}, +{ +"event":"[EMAIL-MASKED]" +}, +{ +"event":"[EMAIL-MASKED]" +}, +{ +"event":"[EMAIL-MASKED]" +}, +{ +"event":"[EMAIL-MASKED]" +}, +{ +"event":"[EMAIL-MASKED]" +}, +{ +"event":"notify: [EMAIL-MASKED]" +}, +{ +"event":"'email': '[EMAIL-MASKED]'" +}, +{ +"event":"\"email\": \"[EMAIL-MASKED]\"" +}, +{ +"event":"email=[EMAIL-MASKED]" +}, +{ +"event":"{\"contact\": {\"email\": \"[EMAIL-MASKED]\"}}" +}, +{ +"event":"[JWT-MASKED]" +}, +{ +"event":"Here it's [JWT-MASKED] failed!" +}, +{ +"event":"Here it's ([JWT-MASKED]) failed!" +}, +{ +"event":"Here it's: [JWT-MASKED] failed!" +}, +{ +"event":"-[JWT-MASKED]" +}, +{ +"event":".[JWT-MASKED]" +}, +{ +"event":"[JWT-MASKED]" +}, +{ +"event":"[JWT-MASKED]" +}, +{ +"event":"[JWT-MASKED]" +}, +{ +"event":"[JWT-MASKED]" +}, +{ +"event":"token was [JWT-MASKED] and more" +}, +{ +"event":"********8912" +}, +{ +"event":"*********9123" +}, +{ +"event":"**********1234" +}, +{ +"event":"912345*****2345" +}, +{ +"event":"912345******3456" +}, +{ +"event":"912345*******4567" +}, +{ +"event":"912345********5678" +}, +{ +"event":"912345*********6789" +}, +{ +"event":"********8912" +}, +{ +"event":"*********9123" +}, +{ +"event":"**********1234" +}, +{ +"event":"912345*****2345" +}, +{ +"event":"912345******3456" +}, +{ +"event":"912345*******4567" +}, +{ +"event":"912345********5678" +}, +{ +"event":"912345*********6789" +}, +{ +"event":"********8912" +}, +{ +"event":"*********9123" +}, +{ +"event":"**********1234" +}, +{ +"event":"912345*****2345" +}, +{ +"event":"912345******3456" +}, +{ +"event":"912345*******4567" +}, +{ +"event":"912345********5678" +}, +{ +"event":"912345*********6789" +}, +{ +"event":"********8912" +}, +{ +"event":"*********9123" +}, +{ +"event":"**********1234" +}, +{ +"event":"112345*****2345" +}, +{ +"event":"112345******3456" +}, +{ +"event":"112345*******4567" +}, +{ +"event":"112345********5678" +} +], +"no-keyset/every-pack/prose":[ +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +"{\"session_id\": \"21a2496f8b6c193ec61b8dd52cbe8d827b4eaa49\", \"order_no\": \"ORD-2026-000123\", \"amount\": \"12.500\", \"currency_code\": \"KWD\", \"customer_email\": \"[EMAIL-MASKED]\", \"customer_phone\": \"[PHONE-MASKED]\", \"customer_first_name\": \"[NAME-MASKED]\", \"billing_address\": {\"line1\": \"[ADDRESS-MASKED]\", \"city\": \"[ADDRESS-MASKED]\", \"country\": \"[ADDRESS-MASKED]\"}, \"pg_codes\": [\"mpgs-direct\", \"cs-direct\", \"kpay\"], \"redirect_url\": \"https://merchant.example/return?ref=abc123&lang=en\", \"webhook_url\": \"https://merchant.example/hooks/ottu\", \"extra\": {\"note\": \"gift wrap\", \"items\": [{\"sku\": \"SKU-0\", \"qty\": 0}, {\"sku\": \"SKU-1\", \"qty\": 1}, {\"sku\": \"SKU-2\", \"qty\": 2}, {\"sku\": \"SKU-3\", \"qty\": 3}, {\"sku\": \"SKU-4\", \"qty\": 4}, {\"sku\": \"SKU-5\", \"qty\": 5}, {\"sku\": \"SKU-6\", \"qty\": 6}, {\"sku\": \"SKU-7\", \"qty\": 7}, {\"sku\": \"SKU-8\", \"qty\": 8}, {\"sku\": \"SKU-9\", \"qty\": 9}]}, \"card_acceptance_criteria\": {\"min_expiry_time\": 30}, \"three_ds\": {\"directory_server_transaction_id\": \"[PAYMENT-ID-MASKED]\"}}", +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null +], +"no-keyset/every-pack/records":[ +[ +"user %s paid %s KWD for %s", +"('bob', '12.500', 'ORD-1')", +"user bob paid 12.500 KWD for ORD-1" +], +[ +"cvv=[CVV-MASKED]", +"()", +"cvv=[CVV-MASKED]" +], +[ +"card 411111******1111 [CVV-MASKED]", +"()", +"card 411111******1111 [CVV-MASKED]" +], +[ +"password=[SECRET-MASKED]", +"()", +"password=[SECRET-MASKED]" +], +[ +"paid 10 KWD by [PHONE-MASKED]", +"()", +"paid 10 KWD by [PHONE-MASKED]" +], +[ +"sent to %s", +"('[EMAIL-MASKED]',)", +"sent to [EMAIL-MASKED]" +], +[ +"apple pay token %s", +"('[SAD-MASKED]',)", +"apple pay token [SAD-MASKED]" +], +[ +"retried with Bearer [SECRET-MASKED]", +"()", +"retried with Bearer [SECRET-MASKED]" +], +[ +"%s", +"{'card_number': '411111******1111', 'cvv': '[CVV-MASKED]'}", +"{'card_number': '411111******1111', 'cvv': '[CVV-MASKED]'}" +] +], +"no-keyset/every-pack/text":[ +"[PEM-KEY-MASKED]", +"[PEM-KEY-MASKED]", +"[PEM-KEY-MASKED]", +"[PEM-KEY-MASKED]", +"[PEM-KEY-MASKED]", +"'token': '[SECRET-MASKED]'", +"'token':'[SECRET-MASKED]'", +"\"token\": \"[SECRET-MASKED]\"", +"\"token\":\"[SECRET-MASKED]\"", +"token: [SECRET-MASKED]", +"token:[SECRET-MASKED]", +"token= [SECRET-MASKED]", +"token=[SECRET-MASKED]", +"token=[SECRET-MASKED]", +"token=[SECRET-MASKED]", +"token: [SECRET-MASKED]", +"token: [SECRET-MASKED]", +"token:[SECRET-MASKED]", +"token=[SECRET-MASKED]", +"token:[SECRET-MASKED]", +"token=[SECRET-MASKED]", +"token [SECRET-MASKED]", +"token [SECRET-MASKED]", +"token [SECRET-MASKED]", +"message token=[SECRET-MASKED]", +"anyword_token [SECRET-MASKED]", +"any_word_token [SECRET-MASKED]", +"any-word_token [SECRET-MASKED]", +"any-word-token [SECRET-MASKED]", +"any_1-word_2-token [SECRET-MASKED]", +"secret [SECRET-MASKED]", +"any_1-word_2-secret [SECRET-MASKED]", +"password [SECRET-MASKED]", +"any_1-word_2-password [SECRET-MASKED]", +"passwd [SECRET-MASKED]", +"any_1-word_2-passwd [SECRET-MASKED]", +"bearer [SECRET-MASKED]", +"basic [SECRET-MASKED]", +"digest [SECRET-MASKED]", +"credential [SECRET-MASKED]", +"credentials [SECRET-MASKED]", +"authorization [SECRET-MASKED]", +"authorization_header [SECRET-MASKED]", +"authorization-header [SECRET-MASKED]", +"authorisation [SECRET-MASKED]", +"authorisation_header [SECRET-MASKED]", +"authorisation-header [SECRET-MASKED]", +"apikey [SECRET-MASKED]", +"api_key [SECRET-MASKED]", +"[SECRET-MASKED]", +"secret-key [SECRET-MASKED]", +"private-key [SECRET-MASKED]", +"public-key [SECRET-MASKED]", +"encryption-key [SECRET-MASKED]", +"decryption-key [SECRET-MASKED]", +"signing-key [SECRET-MASKED]", +"access-key [SECRET-MASKED]", +"master-key [SECRET-MASKED]", +"root-key [SECRET-MASKED]", +"session-key [SECRET-MASKED]", +"secret_key=[SECRET-MASKED]", +"'Authorization': '[SECRET-MASKED]'", +"\"Authorization\": \"[SECRET-MASKED]\"", +"Authorization: [SECRET-MASKED]", +"Authorization= [SECRET-MASKED]", +"Authorization Bearer [SECRET-MASKED]", +"{\"Authorization\": \"[SECRET-MASKED]\"}", +"Here is {\"Authorization\": \"[SECRET-MASKED]\"}", +"{\"Authorization\": \"[SECRET-MASKED]\"}", +"Authorization: [SECRET-MASKED]", +"{\"Authorisation\": \"[SECRET-MASKED]\"}", +"Authentication failed with key '[SECRET-MASKED]'", +"access_token=[SECRET-MASKED]", +"refresh_token=[SECRET-MASKED]", +"client_secret=[SECRET-MASKED]", +"private_key=[SECRET-MASKED]", +"secret_key=[SECRET-MASKED]", +"basic= [SECRET-MASKED]", +"[SECRET-MASKED]", +"Basic [SECRET-MASKED]", +null, +null, +null, +"body {\"session_key\": \"[SECRET-MASKED]\"}", +"body {'api_key': '[SECRET-MASKED]'}", +"{\"result\": \"SUCCESS\", \"session\": {\"aes256Key\": \"[SECRET-MASKED]\", \"id\": \"SESS", +"aes256Key=[SECRET-MASKED]", +"hmacSha256Key=[SECRET-MASKED]", +"privateKeyPem=[SECRET-MASKED]", +null, +"'cvv': '[CVV-MASKED]'", +"'Cvv':'[CVV-MASKED]'", +"\"cVv\": \"[CVV-MASKED]\"", +"\"cvV\":\"[CVV-MASKED]\"", +"CVv: [CVV-MASKED]", +"cVV:[CVV-MASKED]", +"CvV= [CVV-MASKED]", +"CVV=[CVV-MASKED]", +"Here cvv=[CVV-MASKED] is submitted", +"'cvc': '[CVV-MASKED]'", +"'CVC': '[CVV-MASKED]'", +"{\"processed_data\": {\"cvv\": \"[CVV-MASKED]\"}}", +"data {\"cvv\": \"[CVV-MASKED]\"}", +"payment_id: [PAYMENT-ID-MASKED]", +"payment_id= [PAYMENT-ID-MASKED]", +"payment_id [PAYMENT-ID-MASKED]", +"payment-id: [PAYMENT-ID-MASKED]", +"payment-id= [PAYMENT-ID-MASKED]", +"payment-id [PAYMENT-ID-MASKED]", +"'payment_id': '[PAYMENT-ID-MASKED]'", +"'payment-id': '[PAYMENT-ID-MASKED]'", +"'payment_id':'[PAYMENT-ID-MASKED]'", +"'payment-id':'[PAYMENT-ID-MASKED]'", +"\"payment_id\": \"[PAYMENT-ID-MASKED]\"", +"\"payment-id\": \"[PAYMENT-ID-MASKED]\"", +"\"payment_id\":\"[PAYMENT-ID-MASKED]\"", +"\"payment-id\":\"[PAYMENT-ID-MASKED]\"", +"'transaction_id': '[PAYMENT-ID-MASKED]'", +"'auth_id': '[PAYMENT-ID-MASKED]'", +"payment_id: [PAYMENT-ID-MASKED] done", +"payment_id= [PAYMENT-ID-MASKED] done", +"transaction_id: [PAYMENT-ID-MASKED]", +"transaction-id: [PAYMENT-ID-MASKED]", +"auth_id: [PAYMENT-ID-MASKED]", +"auth-id: [PAYMENT-ID-MASKED]", +"{\"processed_data\": {\"payment_id\": \"[PAYMENT-ID-MASKED]\"}}", +"account [IBAN-MASKED] credited", +"acct [IBAN-MASKED] debited", +"acct [IBAN-MASKED] debited", +"acct [IBAN-MASKED] debited", +"acct [IBAN-MASKED] debited", +"acct [IBAN-MASKED] debited", +"acct [IBAN-MASKED] debited", +"acct [IBAN-MASKED] debited", +"acct [IBAN-MASKED] debited", +"[PHONE-MASKED]", +"[PHONE-MASKED]", +"[PHONE-MASKED]", +"[PHONE-MASKED]", +"[PHONE-MASKED]", +"[PHONE-MASKED]", +"[PHONE-MASKED]", +"call [PHONE-MASKED] now", +"call [PHONE-MASKED] now", +"[EMAIL-MASKED]", +"contact [EMAIL-MASKED] now", +"[EMAIL-MASKED]", +"[EMAIL-MASKED]", +"[EMAIL-MASKED]", +"[EMAIL-MASKED]", +"[EMAIL-MASKED]", +"[EMAIL-MASKED]", +"[EMAIL-MASKED]", +"notify: [EMAIL-MASKED]", +"'email': '[EMAIL-MASKED]'", +"\"email\": \"[EMAIL-MASKED]\"", +"email=[EMAIL-MASKED]", +"{\"contact\": {\"email\": \"[EMAIL-MASKED]\"}}", +"[JWT-MASKED]", +"Here it's [JWT-MASKED] failed!", +"Here it's ([JWT-MASKED]) failed!", +"Here it's: [JWT-MASKED] failed!", +"-[JWT-MASKED]", +".[JWT-MASKED]", +"[JWT-MASKED]", +"[JWT-MASKED]", +"[JWT-MASKED]", +"[JWT-MASKED]", +"token was [JWT-MASKED] and more", +"********8912", +"*********9123", +"**********1234", +"912345*****2345", +"912345******3456", +"912345*******4567", +"912345********5678", +"912345*********6789", +"********8912", +"*********9123", +"**********1234", +"912345*****2345", +"912345******3456", +"912345*******4567", +"912345********5678", +"912345*********6789", +"********8912", +"*********9123", +"**********1234", +"912345*****2345", +"912345******3456", +"912345*******4567", +"912345********5678", +"912345*********6789", +"********8912", +"*********9123", +"**********1234", +"112345*****2345", +"112345******3456", +"112345*******4567", +"112345********5678", +"112345*********6789", +"********8912", +"*********9123", +"**********1234", +"112345*****2345", +"112345******3456", +"112345*******4567", +"112345********5678", +"112345*********6789", +"********8912", +"*********9123", +"**********1234", +"112345*****2345", +"112345******3456", +"112345*******4567", +"112345********5678", +"112345*********6789", +"903456******1256", +"903456******1256", +"103456******1256", +"103456******1256", +"903456*********0345", +"903456*********0345", +"103456*********0345", +"103456*********0345", +"903456*********6789", +"903456*********6789", +"403456*********6789", +"403456*********6789", +"********1290", +"903456*****0125", +"903456*********6125", +"********1290", +"403456*****0125", +"403456*********6125", +"Here is 912345*********6789 card Number", +"Here is (912345*********6789) card Number", +"Here is [912345*********6789] card Number", +"Here is {912345*********6789} card Number", +"Here num:912345*********6789 card Number", +"Here num=912345*********6789 card Number", +"Here num,912345*********6789 card Number", +"Here num.912345*********6789 card Number", +"********8912", +"112345*****2345", +"112345******3456", +"112345*******4567", +"112345*********6789", +"point 1 912345*********6789", +"point 1 123456*********7891", +"qty 2 411111******1111", +"qty 2 912345*********6789", +"10:00:004111********1111", +"10:00:00 912345*********6789", +"912345**********7891", +"912345**********7891", +"112345 **********7891", +"1123-4567-********7891", +"912345**********7891", +"1123 4567 ********7891", +"ssn [SSN-MASKED] on file", +"ssn [SSN-MASKED] on file", +"ssn [SSN-MASKED] on file", +"ssn [SSN-MASKED] on file", +"ssn [SSN-MASKED] on file", +"ssn [SSN-MASKED] on file", +"ssn [SSN-MASKED] on file", +"ssn [SSN-MASKED] on file", +"ssn [SSN-MASKED] on file", +"ssn=[SSN-MASKED] on file", +"ssn:[SSN-MASKED] on file", +"ssn,[SSN-MASKED] on file", +"ssn.[SSN-MASKED] on file", +"ssn'[SSN-MASKED]' on file", +"ssn\"[SSN-MASKED]\" on file", +"ssn([SSN-MASKED]) on file", +"ssn[[SSN-MASKED]] on file", +"ssn{[SSN-MASKED]} on file", +"[SSN-MASKED] on file", +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +"", +"error: client_secret=\"[SECRET-MASKED]\" rejected", +"", +"x", +"{\"password\": \"[SECRET-MASKED]}", +"{\"id\": 1, \"password\": \"[SECRET-MASKED] x", +"password=[SECRET-MASKED], status=ok", +"password=[SECRET-MASKED];", +"password=[SECRET-MASKED]'", +"password=[SECRET-MASKED]\",411111******1111&x=1", +"password=[SECRET-MASKED]\":cd&x=1", +"password=\"[SECRET-MASKED]\"&x=1", +"password=[SECRET-MASKED]&x=1", +"{\"xml\": \"\"}", +"{\"note\": \"password=\\\"[SECRET-MASKED]\\\"&x=1\"}", +"{\"note\": \"password=[SECRET-MASKED]&x=1\"}", +"{\"password\": \"[SECRET-MASKED]\"}", +"{\"url\": \"https://x?secret=[SECRET-MASKED]\"}", +"{\"password\": \"[SECRET-MASKED]\"}", +"could not connect postgresql://[SECRET-MASKED]:[SECRET-MASKED]@localhost:5432/db", +"amqp://[SECRET-MASKED]:[SECRET-MASKED]@10.0.0.5//", +"GET https://[SECRET-MASKED]:@api.host/ failed", +"redis://:[SECRET-MASKED]@cache:6379/0 refused", +"{\"dsn\": \"mongodb+srv://[SECRET-MASKED]:[SECRET-MASKED]@cluster0.example.net/app\"}", +"fetch https://[SECRET-MASKED]:[SECRET-MASKED]@api.host.example/v1/pay", +"amqp://[SECRET-MASKED]:[SECRET-MASKED]@mq:5672/", +"amqp://[SECRET-MASKED]:[SECRET-MASKED]@mq:5672/", +"sent {\"password\": \"[SECRET-MASKED]\"} to the gateway", +"login with password=[SECRET-MASKED] failed", +"retried with Bearer [SECRET-MASKED]", +"411111******1111 [CVV-MASKED]", +"411111******1111 [CVV-MASKED]", +"411111******1111|12/27|[CVV-MASKED]", +"pan 411111******1111 12/27 [CVV-MASKED] ok", +"411111******1111,12,27,[CVV-MASKED]", +"411111******1111;[CVV-MASKED];[CVV-MASKED]", +"411111******1111 [CVV-MASKED] [CVV-MASKED]", +"pan 411111******1111 [CVV-MASKED]", +"card 411111******1111 exp 12/27 [CVV-MASKED]", +"card 411111******1111 expiry: 1227 [CVV-MASKED]", +"411111******1111 [CVV-MASKED] 12/27", +"411111******1111\n[CVV-MASKED]", +"paid with 411111******1111 [CVV-MASKED].", +"411111******1111: [CVV-MASKED]", +"********0002 [CVV-MASKED]", +"********1111 [CVV-MASKED]", +"REF4111111111111111 [CVV-MASKED]", +"the cvv is [CVV-MASKED]", +"security code was [CVV-MASKED]", +"cvv - [CVV-MASKED] entered", +"CVV2 # [CVV-MASKED]", +"cvv number [CVV-MASKED]", +"card 411111******1111 cvv [CVV-MASKED]", +"cvv=[CVV-MASKED] for 411111******1111", +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +"TRANPORTAL123[SECRET-MASKED]110.000TRK1{\"paymentData\":\"[SAD-MASKED]\"}", +"TRANPORTAL123[SECRET-MASKED]810.000202612345TRK1TrackID", +"TRANPORTAL123[SECRET-MASKED]25.000202612345TRK1TrackID", +"{\"session_id\": \"21a2496f8b6c193ec61b8dd52cbe8d827b4eaa49\", \"order_no\": \"ORD-2026-000123\", \"amount\": \"12.500\", \"currency_code\": \"KWD\", \"customer_email\": \"[EMAIL-MASKED]\", \"customer_phone\": \"[PHONE-MASKED]\", \"customer_first_name\": \"Jane\", \"billing_address\": {\"line1\": \"Block 1, Street 2\", \"city\": \"Kuwait City\", \"country\": \"KW\"}, \"pg_codes\": [\"mpgs-direct\", \"cs-direct\", \"kpay\"], \"redirect_url\": \"https://merchant.example/return?ref=abc123&lang=en\", \"webhook_url\": \"https://merchant.example/hooks/ottu\", \"extra\": {\"note\": \"gift wrap\", \"items\": [{\"sku\": \"SKU-0\", \"qty\": 0}, {\"sku\": \"SKU-1\", \"qty\": 1}, {\"sku\": \"SKU-2\", \"qty\": 2}, {\"sku\": \"SKU-3\", \"qty\": 3}, {\"sku\": \"SKU-4\", \"qty\": 4}, {\"sku\": \"SKU-5\", \"qty\": 5}, {\"sku\": \"SKU-6\", \"qty\": 6}, {\"sku\": \"SKU-7\", \"qty\": 7}, {\"sku\": \"SKU-8\", \"qty\": 8}, {\"sku\": \"SKU-9\", \"qty\": 9}]}, \"card_acceptance_criteria\": {\"min_expiry_time\": 30}, \"three_ds\": {\"directory_server_transaction_id\": \"3d85c5d3-8412-4f1d-9e26-4a0f00123456\"}}", +null, +"[SAD-MASKED][EMAIL-MASKED]]", +"https://[SECRET-MASKED]@h.example/", +"[the cvv is [PHONE-MASKED]", +"********9350 mshttps://[SECRET-MASKED]@h.example/[SAD-MASKED],", +"Bearer [SECRET-MASKED]\": \"", +null, +"Authorization: [SECRET-MASKED][)", +null, +"12/27abc123\"{{,[EMAIL-MASKED] cvv is )", +"([JWT-MASKED]+96550000000token=[EMAIL-MASKED],{\"[SAD-MASKED]\"12/27", +"}cvv https://[SECRET-MASKED]:[SECRET-MASKED]@h/"", +null, +"https://[SECRET-MASKED]:[SECRET-MASKED]@h/abc123cvv=:cvv |", +null, +"[EMAIL-MASKED]\\", +null, +"350 ms\\token=[SECRET-MASKED]}password=}{Authorization: \"[SECRET-MASKED]\": \"", +"1234Bearer \"https://[SECRET-MASKED]:[SECRET-MASKED]@h/[SAD-MASKED]\"token\": \"", +"[CVV-MASKED](", +"200\"[SAD-MASKED]\"12/27)jane@example.com123411111111111111112/27200", +null, +null, +"123[cvv=,[PHONE-MASKED]", +",(\"[SAD-MASKED]\"Authorization: [SECRET-MASKED]>", +"[IBAN-MASKED],\"token\": \"[SECRET-MASKED]>", +"\"[SAD-MASKED]\"https://[SECRET-MASKED]@h.example/ (350 msabc123\"350 mspassword=", +"\\https://[SECRET-MASKED]@h.example/", +",["the cvv is [JWT-MASKED]", +"https://[SECRET-MASKED]:[SECRET-MASKED]@h/411111******1111\"token\": \"}"the cvv is Bearer password=", +"(cvv [SAD-MASKED]cvv=[EMAIL-MASKED]:[JWT-MASKED]", +"[PHONE-MASKED]cvv=350 ms", +null, +"{password=[SECRET-MASKED] cvv is [IBAN-MASKED]{abc123123-45-6789123-45-6789", +"[SAD-MASKED]password='}", +null, +"ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAtoken=payment_id=[PAYMENT-ID-MASKED]cvv ", +"][EMAIL-MASKED](ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\"https://[SECRET-MASKED]:[SECRET-MASKED]@h/]", +"Authorization: [SECRET-MASKED]", +"\"token\": \"[SECRET-MASKED]&password=[PEM-KEY-MASKED](Authorization: [SECRET-MASKED]", +"123|(411111******1111cvv [CVV-MASKED]", +"|123-45-6789}cvv=the cvv is [SAD-MASKED] 200", +"|https://[SECRET-MASKED]:[SECRET-MASKED]@h/\"[SAD-MASKED]\"{", +"{\"[SAD-MASKED]\"abc123", +null, +":\"[SAD-MASKED]\"cvv=https://[SECRET-MASKED]@h.example/payment_id=[PAYMENT-ID-MASKED] cvv is [JWT-MASKED]", +"][411111******1111GB33BUKB20201555555555", +null, +null, +"|'(:)[SAD-MASKED]{[ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", +":\"[SAD-MASKED]\"}123412/27200GB33BUKB20201555555555'", +null, +"\"token\": \"[SECRET-MASKED][SAD-MASKED]411111******1111}123-45-6789", +"cvv [CVV-MASKED]\"[SAD-MASKED]\"|12/27{[IBAN-MASKED] ms", +"+96550000000token=Authorization: [SECRET-MASKED] cvv is \"token\": \"", +"200payment_id=[PAYMENT-ID-MASKED]=:\"&", +null, +"Bearer 350 ms123-45-6789:[EMAIL-MASKED] ", +"[IBAN-MASKED](https://[SECRET-MASKED]@h.example/:]ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA123412/27the cvv is ", +"cvv token=[SECRET-MASKED]", +null, +"s3cr3tpassword='payment_id=[PAYMENT-ID-MASKED]abc123GB33BUKB20201555555555", +"[SSN-MASKED]******1111GB33BUKB20201555555555abc123123\"[SAD-MASKED]\"cvv ", +null, +"[PHONE-MASKED]", +"https://[SECRET-MASKED]@h.example/123the cvv is }", +"|payment_id=[PAYMENT-ID-MASKED]350 mscvv=", +"'s3cr3thttps://[SECRET-MASKED]:[SECRET-MASKED]@h/[password=[SECRET-MASKED]:'", +"password=[SECRET-MASKED] cvv='jane@example.comGB33BUKB20201555555555", +null, +"411111******1111s3cr3tGB33BUKB20201555555555 cvv=", +"]411111******1111123-45-6789cvv=411111******1111[SAD-MASKED])", +"[SSN-MASKED]******111112/27350 ms:{350 ms", +null, +"|[JWT-MASKED]", +"\\Bearer payment_id=[PAYMENT-ID-MASKED]\"token\": \"]", +"411111******1111https://u:p@h/411111******1111\\" 200", +null, +null, +"411111********1112/27GB33BUKB20201555555555", +""cvv [CVV-MASKED]\"[SAD-MASKED]\"\"[SAD-MASKED]\"\"Authorization: ", +"[PEM-KEY-MASKED]abc123,200350 ms\"token\": \"[", +"[Bearer [SECRET-MASKED]", +"\\123-45-6789https://u:p@h/https://[SECRET-MASKED]:[SECRET-MASKED]@h/", +"411111******1111cvv \\]", +"&350 ms&',Authorization: [SECRET-MASKED]", +"&123-45-6789Bearer https://[SECRET-MASKED]@h.example/]200jane@example.comGB33BUKB20201555555555abc123", +")[EMAIL-MASKED] cvv is ", +"}[JWT-MASKED]", +null, +"\"[SAD-MASKED]\"411111******1111'[the cvv is payment_id=[PAYMENT-ID-MASKED]=token=", +null, +"(} 411111******1111", +null, +"jane@example.com350 ms\"[SAD-MASKED]\"\"[JWT-MASKED]&", +" [EMAIL-MASKED]+96550000000cvv=&]", +"\"[PHONE-MASKED][SAD-MASKED]|[SAD-MASKED] (", +null, +"\"token=[SECRET-MASKED]>[PEM-KEY-MASKED]", +"123-45-678912/27eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.c2lnptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[SAD-MASKED]\"token\": \"[SECRET-MASKED]", +null, +"+96550000000cvv [JWT-MASKED]\\https://[SECRET-MASKED]@h.example/", +null, +"[SAD-MASKED]", +"payment_id=[PAYMENT-ID-MASKED]", +"Authorization: [SECRET-MASKED] ms", +"}|[JWT-MASKED]", +"]350 [EMAIL-MASKED]=s3cr3teyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.c2ln", +"[IBAN-MASKED]|[EMAIL-MASKED]@example.com123+96550000000+96550000000[", +" [SSN-MASKED]&abc123[EMAIL-MASKED]:", +"(token=[SECRET-MASKED]>(password=the cvv is [IBAN-MASKED]", +"payment_id=[PAYMENT-ID-MASKED](:}411111******1111}", +"200https://[SECRET-MASKED]:[SECRET-MASKED]@h/ the cvv is https://[SECRET-MASKED]:[SECRET-MASKED]@h/[PEM-KEY-MASKED])password=", +"Bearer [SECRET-MASKED][SAD-MASKED]", +null, +"[SAD-MASKED]|411111******1111{[SECRET-MASKED]payment_id=[PAYMENT-ID-MASKED]******1111token=", +"12/27\"[SAD-MASKED]\"350 ms123-45-6789[", +"\"[SAD-MASKED]\"[SSN-MASKED]the cvv is ]", +null, +"'the cvv is https://[SECRET-MASKED]@h.example/"cvv=", +"[JWT-MASKED]=)", +null, +null, +null, +"token=\"[SAD-MASKED]\"token=", +"': [EMAIL-MASKED]{https://[SECRET-MASKED]:[SECRET-MASKED]@h/", +"200&:\"[SAD-MASKED]\""411111******1111the cvv is \"[SAD-MASKED]\"", +"]the cvv is [SSN-MASKED]", +"https://[SECRET-MASKED]@h.example/[cvv s3cr3t\"", +":(|the cvv is password=[SECRET-MASKED]>", +null, +null, +"[PEM-KEY-MASKED]'(\\https://[SECRET-MASKED]:[SECRET-MASKED]@h/&", +null, +"123456********6789{350 msabc123[", +" (password=[SECRET-MASKED] ", +",[JWT-MASKED]", +"]|\"token\": \"[SECRET-MASKED],|123-45-6789payment_id=abc12345", +"{[PEM-KEY-MASKED]\"token\": \"[SECRET-MASKED] cvv=}200", +"12/27\\|https://[SECRET-MASKED]:[SECRET-MASKED]@h/[IBAN-MASKED]\"token\": \"", +"]411111******1111\\", +null, +"Authorization: [SECRET-MASKED]", +"123 [EMAIL-MASKED]=", +"411111******1111s3cr3t1234}cvv ,}cvv=[PHONE-MASKED]", +"123[PEM-KEY-MASKED]cvv password="s3cr3thttps://[SECRET-MASKED]:[SECRET-MASKED]@h/token=[SECRET-MASKED]", +"&)\"411111******1111\\[", +":payment_id=[PAYMENT-ID-MASKED]}https://[SECRET-MASKED]:[SECRET-MASKED]@h/password=\"", +"123-45-6789https://[EMAIL-MASKED]/411111******1111", +"[EMAIL-MASKED]\"[SAD-MASKED]\" ", +"[PEM-KEY-MASKED]]\"token\": \"&abc123\\the cvv is the cvv is [SSN-MASKED](", +"\\[EMAIL-MASKED],cvv=,)", +null, +"'350 mshttps://[SECRET-MASKED]@h.example/'\\the cvv is ", +"https://[SECRET-MASKED]:[SECRET-MASKED]@h/password=\"[SECRET-MASKED]&123", +"\\cvv \"[SAD-MASKED]\"", +"411111******1111https://u:p@h/password=", +"https://[SECRET-MASKED]:[SECRET-MASKED]@h/ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAuthorization: 4111111111111111123412/27Bearer [SECRET-MASKED]", +": GB33BUKB20201555555555eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.c2ln [PEM-KEY-MASKED]{ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", +"\",cvv |abc123ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA123-45-6789[PEM-KEY-MASKED]", +null, +"\"|411111******1111}411111******1111[SECRET-MASKED]", +": ]cvv=https://[SECRET-MASKED]:[SECRET-MASKED]@h/", +"\"[SAD-MASKED]\")}", +"https://[SECRET-MASKED]@h.example/:the cvv is Bearer &the cvv is \"[EMAIL-MASKED]+96550000000", +null, +"password=[SECRET-MASKED]],password=", +null, +"12/27Bearer 123Authorization: password=[SECRET-MASKED][SECRET-MASKED]abc123Bearer payment_id=[PAYMENT-ID-MASKED]", +null, +null, +"[IBAN-MASKED]", +null, +"token=[SECRET-MASKED] cvv is ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAhttps://[SECRET-MASKED]@h.example/Bearer cvv ", +null, +"\\)\"[SAD-MASKED]\"\\the cvv is [PEM-KEY-MASKED]", +null, +"|the cvv is https://[SECRET-MASKED]@h.example/ cvv ", +"[EMAIL-MASKED]'\"|\"411111******1111abc1231234s3cr3t", +null, +"Authorization: [SECRET-MASKED]&", +"payment_id=[PAYMENT-ID-MASKED][JWT-MASKED]\"[SAD-MASKED]\"https://[SECRET-MASKED]@h.example/Bearer \\", +" payment_id=[PAYMENT-ID-MASKED]", +"Authorization: [SECRET-MASKED][SAD-MASKED]"&", +null, +"[SAD-MASKED]200s3cr3t123", +"([PEM-KEY-MASKED]]password=[SECRET-MASKED]https://[SECRET-MASKED]@h.example/", +null, +null, +"\"token\": \"[SECRET-MASKED] ms12/27350 mseyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.c2ln", +"&+96550000000]https://[SECRET-MASKED]:[SECRET-MASKED]@h/411111******1111{\"", +"[PHONE-MASKED]\"[SECRET-MASKED]'s3cr3t\"411111******1111GB33BUKB20201555555555s3cr3t[SECRET-MASKED]", +"https://[SECRET-MASKED]@h.example/'{\\", +null, +"[SSN-MASKED]******1111", +"[EMAIL-MASKED] ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA350 msAuthorization: https://[SECRET-MASKED]:[SECRET-MASKED]@h/411111******1111", +null, +"[,Authorization: [PEM-KEY-MASKED]ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAuthorization: ", +"411111******1111[PEM-KEY-MASKED]200cvv 411111******1111\"12/27https://u:p@h/", +"the cvv is (s3cr3t'https://[SECRET-MASKED]:[SECRET-MASKED]@h/[EMAIL-MASKED] cvv is \"token\": \"[SECRET-MASKED][SAD-MASKED]", +"\"[EMAIL-MASKED]:411111******1111", +null, +"'cvv=ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA350 mshttps://[SECRET-MASKED]:[SECRET-MASKED]@h/ https://[SECRET-MASKED]:[SECRET-MASKED]@h/123", +"{the cvv is [EMAIL-MASKED]=200"200}", +"[SAD-MASKED]\\\\", +"[PEM-KEY-MASKED] ]{s3cr3t]|[SAD-MASKED]", +"Bearer )411111******1111'[IBAN-MASKED]", +"411111******1111])}https://[SECRET-MASKED]:[SECRET-MASKED]@h/", +"https://[SECRET-MASKED]:[SECRET-MASKED]@h/Authorization: [SECRET-MASKED] |[SECRET-MASKED]200350 ms", +"200411*********1111411111******1111\"ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAhttps://[EMAIL-MASKED]/ \"[SAD-MASKED]\"{", +null, +"411111******1111eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.c2ln,\"[SAD-MASKED]\"[IBAN-MASKED]|\\", +null, +"Authorization: )'('[PEM-KEY-MASKED]the cvv is the cvv is "", +"][SAD-MASKED]&", +"Authorization: ,https://[SECRET-MASKED]@h.example/", +"1234cvv=password=\"[SECRET-MASKED]\": \"", +"12/27\"[SAD-MASKED]\"Authorization: ", +" s3cr3tpayment_id=[PAYMENT-ID-MASKED]", +"{Authorization: 200([PEM-KEY-MASKED]ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", +"payment_id=[PAYMENT-ID-MASKED]/27 ", +null, +"https://[SECRET-MASKED]@h.example/cvv=[PHONE-MASKED]/27token=12/27", +"]200https://[EMAIL-MASKED]/350 ms"", +null, +null, +""https://[SECRET-MASKED]:[SECRET-MASKED]@h/|ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", +"password=[PEM-KEY-MASKED]", +null, +null, +" (1234ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAtoken=\"[SAD-MASKED]\"cvv=jane@example.com200", +"[https://[SECRET-MASKED]@h.example/ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA411111******1111\"]", +null, +"\"token\": \"[SECRET-MASKED]\" [[PEM-KEY-MASKED]token=[SECRET-MASKED]", +""123\"token\": \"[SECRET-MASKED] ", +"Bearer https://[SECRET-MASKED]@h.example/'", +"[IBAN-MASKED]\"[SAD-MASKED]\"https://[SECRET-MASKED]@h.example/\"token\": \"[SECRET-MASKED]}[PEM-KEY-MASKED]abc123", +null, +null, +"Bearer [SECRET-MASKED]>\\]\\", +null, +"https://[SECRET-MASKED]:[SECRET-MASKED]@h/cvv cvv [SAD-MASKED]'", +"[SECRET-MASKED]&411111******1111&https://[SECRET-MASKED]:[SECRET-MASKED]@h/\\[SECRET-MASKED]", +"}"cvv=][SECRET-MASKED][SECRET-MASKED]token=[SECRET-MASKED] 411111******1111", +"[PEM-KEY-MASKED]", +")https://[SECRET-MASKED]:[SECRET-MASKED]@h/", +null, +"[SAD-MASKED]cvv ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[EMAIL-MASKED] cvv is }abc123", +"Authorization: [SECRET-MASKED]", +"cvv token=[IBAN-MASKED]******1111https://[EMAIL-MASKED]/:[EMAIL-MASKED]", +"\"[SAD-MASKED]\"cvv cvv=[SECRET-MASKED]jane@example.com411111******1111", +"s3cr3t[SAD-MASKED]cvv=\\1234", +"Bearer (\"[SAD-MASKED]\"", +"[SSN-MASKED]{[[EMAIL-MASKED]:Bearer "", +null, +"[IBAN-MASKED]\"token\": \"[SECRET-MASKED][SAD-MASKED][SAD-MASKED],"]", +"https://[SECRET-MASKED]@h.example/token=[SECRET-MASKED] ms]https://[SECRET-MASKED]:[SECRET-MASKED]@h/\"|{password=", +"[&123-45-6789411111******1111+96550000000[SAD-MASKED]abc123GB33BUKB20201555555555", +"[SECRET-MASKED]411111******1111"cvv=[JWT-MASKED][EMAIL-MASKED] cvv is ", +"[SECRET-MASKED]"Authorization: \"[SECRET-MASKED]\": \"", +null, +"cvv [CVV-MASKED][SAD-MASKED] Authorization: [SECRET-MASKED]", +null, +"\"[SAD-MASKED]\"&", +"Bearer }s3cr3tBearer [CVV-MASKED]", +"1234[SECRET-MASKED]\"[SAD-MASKED]\"ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAthe cvv is ", +"\"[SAD-MASKED]\"the cvv is 411111******1111'", +null, +null, +null, +"[EMAIL-MASKED],", +"[JWT-MASKED]\"[SECRET-MASKED]https://[SECRET-MASKED]:[SECRET-MASKED]@h/", +"|"&[PEM-KEY-MASKED]]350 ms123-45-6789", +"Bearer \"[SAD-MASKED]\"200\"[SAD-MASKED]\",:[PHONE-MASKED]}abc123", +"token=[SECRET-MASKED]", +null, +"(https://[SECRET-MASKED]@h.example/][JWT-MASKED]", +"Authorization: [CVV-MASKED]", +"(\"[SAD-MASKED]\"411111******11111234payment_id=abc12345", +null, +")}https://[SECRET-MASKED]@h.example/[PEM-KEY-MASKED]https://[SECRET-MASKED]:[SECRET-MASKED]@h/'[EMAIL-MASKED][SECRET-MASKED]+96550000000123-45-6789", +"\"[SAD-MASKED]\"200eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.c2ln[SECRET-MASKED]cvv ", +" the cvv is 411111******1111\"}Bearer [SAD-MASKED]", +"}Bearer token=[SECRET-MASKED])", +null, +"s3cr3ttoken=ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[SAD-MASKED]cvv=,password=", +"[IBAN-MASKED]", +"********9200)s3cr3t(Bearer [SECRET-MASKED]", +"411111*********1123-45-6789[1234password= cvv=token=\"[SECRET-MASKED]", +"https://[SECRET-MASKED]@h.example/[EMAIL-MASKED]", +"password="411111******1111:payment_id=[PAYMENT-ID-MASKED] mscvv=\"cvv=", +"abc123https://[SECRET-MASKED]:[SECRET-MASKED]@h/Bearer &(", +"[SAD-MASKED]]\"Authorization: [SECRET-MASKED]" +], +"no-keyset/redact_body":[ +null, +null, +null, +null, +null, +null, +null, +"\"token\": \"[SECRET-MASKED]\"", +"\"token\":\"[SECRET-MASKED]\"", +null, +null, +null, +"token=[SECRET-MASKED]", +"token=[SECRET-MASKED]", +"token=[SECRET-MASKED]", +null, +null, +null, +"token=[SECRET-MASKED]", +null, +"token=[SECRET-MASKED]", +null, +null, +null, +"message token=[SECRET-MASKED]", +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +"access_token=[SECRET-MASKED]", +"refresh_token=[SECRET-MASKED]", +"client_secret=[SECRET-MASKED]", +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +"CVV=[CVV-MASKED]", +"Here cvv=[CVV-MASKED] is submitted", +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +"", +"error: client_secret=\"[SECRET-MASKED]\" rejected", +"", +"", +"{\"password\": \"[SECRET-MASKED]\"", +"{\"id\": 1, \"password\": \"[SECRET-MASKED]\"", +"password=[SECRET-MASKED], status=ok", +"password=[SECRET-MASKED]", +"password=[SECRET-MASKED]", +"password=[SECRET-MASKED]&x=1", +"password=[SECRET-MASKED]&x=1", +"password=\"[SECRET-MASKED]\"&x=1", +"password=[SECRET-MASKED]&x=1", +"{\"xml\": \"\"}", +"{\"note\": \"password=\\\"[SECRET-MASKED]\\\"&x=1\"}", +"{\"note\": \"password=[SECRET-MASKED]&x=1\"}", +"{\"password\": \"[SECRET-MASKED]\"}", +"{\"url\": \"https://x?secret=[SECRET-MASKED]\"}", +"{\"password\": \"[SECRET-MASKED]\"}", +null, +null, +null, +null, +null, +null, +null, +null, +"sent {\"password\": \"[SECRET-MASKED]\"} to the gateway", +"login with password=[SECRET-MASKED] failed", +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +"cvv=[CVV-MASKED] for 411111******1111", +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +null, +"TRANPORTAL123[SECRET-MASKED]110.000TRK1{\"paymentData\":\"[SAD-MASKED]\"}", +"TRANPORTAL123[SECRET-MASKED]810.000202612345TRK1TrackID", +"TRANPORTAL123[SECRET-MASKED]25.000202612345TRK1TrackID", +null, +null, +"[SAD-MASKED]jane@example.com]", +null, +null, +"123-45-6789350 mshttps://4111111111111111@h.example/[SAD-MASKED],", +"Bearer |GB33BUKB20201555555555\"token\": \"\"", +"123cvv=[CVV-MASKED](", +null, +null, +null, +"(eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.c2ln+96550000000token=jane@example.com,{\"[SAD-MASKED]\"12/27", +null, +null, +"https://u:p@h/abc123cvv=[CVV-MASKED] |", +null, +null, +null, +"350 ms\\token=[SECRET-MASKED] \"token\": \"\"", +"1234Bearer \"https://u:p@h/[SAD-MASKED]\"token\": \"\"", +"[CVV-MASKED](", +"200\"[SAD-MASKED]\"12/27)jane@example.com123411111111111111112/27200", +null, +null, +"123[cvv=[CVV-MASKED]", +",(\"[SAD-MASKED]\"Authorization: +96550000000", +"GB33BUKB20201555555555,\"token\": \"[SECRET-MASKED]\"", +"\"[SAD-MASKED]\"https://4111111111111111@h.example/ (350 msabc123\"350 mspassword=", +null, +null, +"https://u:p@h/411111******1111\"token\": \"[SECRET-MASKED]\"", +"(cvv [SAD-MASKED]cvv=jane@example.com:eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.c2ln", +null, +null, +"{password=[SECRET-MASKED] cvv is GB33BUKB20201555555555{abc123123-45-6789123-45-6789", +"[SAD-MASKED]password=[SECRET-MASKED]}", +null, +null, +null, +null, +"\"token\": \"[SECRET-MASKED]\"", +null, +"|123-45-6789}cvv=[CVV-MASKED] cvv is [SAD-MASKED] 200", +"|https://u:p@h/\"[SAD-MASKED]\"{", +"{\"[SAD-MASKED]\"abc123", +null, +":\"[SAD-MASKED]\"cvv=[CVV-MASKED] cvv is eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.c2ln", +null, +null, +null, +"|'(:)[SAD-MASKED]{[ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", +":\"[SAD-MASKED]\"}123412/27200GB33BUKB20201555555555'", +null, +"\"token\": \"[SECRET-MASKED]\"", +"cvv 411111******1111\"[SAD-MASKED]\"|12/27{GB33BUKB20201555555555350 ms", +"+96550000000token=Authorization: the cvv is \"token\": \"\"", +null, +null, +null, +null, +"cvv token=[SECRET-MASKED] +96550000000", +null, +null, +"123411111******1111GB33BUKB20201555555555abc123123\"[SAD-MASKED]\"cvv ", +null, +null, +null, +null, +"'s3cr3thttps://u:p@h/[password=[SECRET-MASKED]'", +"password=[SECRET-MASKED] cvv=[CVV-MASKED]jane@example.comGB33BUKB20201555555555", +null, +null, +"]411111******1111123-45-6789cvv=[CVV-MASKED]", +null, +null, +null, +"\\Bearer payment_id=abc12345123\"token\": \"[SECRET-MASKED]\"", +null, +null, +null, +null, +""cvv 123\"[SAD-MASKED]\"\"[SAD-MASKED]\"\"Authorization: ", +"-----BEGIN RSA PRIVATE KEY-----\nMIIB\n-----END RSA PRIVATE KEY-----abc123,200350 ms\"token\": \"[SECRET-MASKED]\"", +null, +null, +null, +null, +null, +null, +null, +null, +"\"[SAD-MASKED]\"411111******1111'[the cvv is payment_id=abc12345token=token=", +null, +null, +null, +"jane@example.com350 ms\"[SAD-MASKED]\"\"eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.c2ln&", +null, +"\"+96550000000[SAD-MASKED]|[SAD-MASKED] (", +null, +"\"token=[SECRET-MASKED] RSA PRIVATE KEY-----\nMIIB\n-----END RSA PRIVATE KEY-----", +"123-45-678912/27eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.c2lnptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[SAD-MASKED]\"token\": \"[SECRET-MASKED]\"", +null, +null, +null, +"[SAD-MASKED]", +null, +null, +null, +null, +null, +null, +"(token=[SECRET-MASKED] cvv is GB33BUKB20201555555555", +null, +null, +"Bearer https://4111111111111111@h.example/|[SAD-MASKED]", +null, +"[SAD-MASKED]|411111******1111{[SECRET-MASKED]payment_id=abc12345411111******1111token=", +"12/27\"[SAD-MASKED]\"350 ms123-45-6789[", +"\"[SAD-MASKED]\"123-45-6789the cvv is ]", +null, +null, +null, +null, +null, +null, +"token=\"[SECRET-MASKED]", +null, +"200&:\"[SAD-MASKED]\""411111******1111the cvv is \"[SAD-MASKED]\"", +null, +null, +":(|the cvv is password=[SECRET-MASKED]>", +null, +null, +null, +null, +null, +" (password=[SECRET-MASKED] ", +null, +"]|\"token\": \"[SECRET-MASKED]\"", +"{-----BEGIN RSA PRIVATE KEY-----\nMIIB\n-----END RSA PRIVATE KEY-----\"token\": \"[SECRET-MASKED]\"", +"12/27\\|https://u:p@h/GB33BUKB20201555555555\"token\": \"\"", +null, +null, +null, +null, +"411111******1111s3cr3t1234}cvv ,}cvv=[CVV-MASKED]", +"123-----BEGIN RSA PRIVATE KEY-----\nMIIB\n-----END RSA PRIVATE KEY-----cvv password="s3cr3thttps://u:p@h/token=[SECRET-MASKED]", +null, +null, +null, +"jane@example.com\"[SAD-MASKED]\" ", +"-----BEGIN RSA PRIVATE KEY-----\nMIIB\n-----END RSA PRIVATE KEY-----]\"token\": \"[SECRET-MASKED]\"", +"\\jane@example.com,cvv=[CVV-MASKED]>", +"2001234'[\"token\": \"[SECRET-MASKED]\"", +null, +"https://u:p@h/password=\"[SECRET-MASKED]&123", +"\\cvv \"[SAD-MASKED]\"", +null, +null, +null, +null, +null, +null, +": ]cvv=[CVV-MASKED]", +"\"[SAD-MASKED]\")}", +null, +null, +"password=[SECRET-MASKED]", +null, +"12/27Bearer 123Authorization: password=[SECRET-MASKED] payment_id=abc12345", +null, +null, +null, +"\\token=[SECRET-MASKED]", +"token=[SECRET-MASKED] cvv is ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAhttps://4111111111111111@h.example/Bearer cvv ", +null, +"\\)\"[SAD-MASKED]\"\\the cvv is -----BEGIN RSA PRIVATE KEY-----\nMIIB\n-----END RSA PRIVATE KEY-----", +null, +null, +null, +null, +null, +"payment_id=abc12345eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.c2ln\"[SAD-MASKED]\"https://4111111111111111@h.example/Bearer \\", +null, +"Authorization: [SECRET-MASKED][SAD-MASKED]"&", +null, +"[SAD-MASKED]200s3cr3t123", +"(-----BEGIN RSA PRIVATE KEY-----\nMIIB\n-----END RSA PRIVATE KEY-----]password=[SECRET-MASKED]https://4111111111111111@h.example/", +null, +null, +"\"token\": \"[SECRET-MASKED]\"", +null, +null, +null, +null, +null, +null, +null, +null, +null, +"the cvv is (s3cr3t'https://u:p@h/jane@example.comthe cvv is \"token\": \"[SECRET-MASKED]\"", +null, +null, +"'cvv=[CVV-MASKED] mshttps://u:p@h/ https://u:p@h/123", +"{the cvv is jane@example.comcvv=[CVV-MASKED]"200}", +"[SAD-MASKED]\\\\", +"-----BEGIN RSA PRIVATE KEY-----\nMIIB\n-----END RSA PRIVATE KEY----- ]{s3cr3t]|[SAD-MASKED]", +null, +null, +null, +"20041111111111111114111111111111111\"ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAhttps://4111111111111111@h.example/ \"[SAD-MASKED]\"{", +null, +"411111******1111eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.c2ln,\"[SAD-MASKED]\"GB33BUKB20201555555555|\\", +null, +null, +"][SAD-MASKED]&", +null, +"1234cvv=[CVV-MASKED]\": \"", +"12/27\"[SAD-MASKED]\"Authorization: ", +null, +null, +null, +null, +"https://4111111111111111@h.example/cvv=[CVV-MASKED]", +null, +null, +null, +null, +"password=[SECRET-MASKED] RSA PRIVATE KEY-----\nMIIB\n-----END RSA PRIVATE KEY-----", +"\\1234cvv=[CVV-MASKED]|123-45-6789", +null, +" (1234ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAtoken=\"[SAD-MASKED]\"cvv=jane@example.com200", +null, +null, +"\"token\": \"[SECRET-MASKED]\" [-----BEGIN RSA PRIVATE KEY-----\nMIIB\n-----END RSA PRIVATE KEY-----token=[SECRET-MASKED]", +""123\"token\": \"[SECRET-MASKED]\"", +null, +"GB33BUKB20201555555555\"[SAD-MASKED]\"https://4111111111111111@h.example/\"token\": \"[SECRET-MASKED]\"", +null, +null, +null, +null, +"https://u:p@h/cvv cvv [SAD-MASKED]'", +null, +"}"cvv=[CVV-MASKED] 4111111111111111", +null, +null, +null, +"[SAD-MASKED]cvv ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAjane@example.comthe cvv is }abc123", +null, +null, +"\"[SAD-MASKED]\"cvv cvv=[CVV-MASKED]>", +"s3cr3t[SAD-MASKED]cvv=[CVV-MASKED]", +"Bearer (\"[SAD-MASKED]\"", +null, +null, +"GB33BUKB20201555555555123\"token\": \"[SECRET-MASKED]\"", +"https://4111111111111111@h.example/token=[SECRET-MASKED] ms]https://u:p@h/\"|{password=", +"[&123-45-6789411111******1111+96550000000[SAD-MASKED]abc123GB33BUKB20201555555555", +"[SECRET-MASKED]4111111111111111"cvv=[CVV-MASKED]jane@example.comthe cvv is ", +"[SECRET-MASKED]"Authorization: \"token\": \"\"", +null, +"cvv 123[SAD-MASKED] Authorization: eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.c2ln", +null, +"\"[SAD-MASKED]\"&", +"Bearer }s3cr3tBearer [CVV-MASKED]", +"1234[SECRET-MASKED]\"[SAD-MASKED]\"ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAthe cvv is ", +"\"[SAD-MASKED]\"the cvv is 411111******1111'", +null, +null, +null, +null, +null, +null, +"Bearer \"[SAD-MASKED]\"200\"[SAD-MASKED]\",:+96550000000}abc123", +"token=[SECRET-MASKED]]"Bearer \"token\": \"\"", +null, +null, +"Authorization: [CVV-MASKED]", +"(\"[SAD-MASKED]\"41111111111111111234payment_id=abc12345", +null, +null, +"\"[SAD-MASKED]\"200eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.c2ln[SECRET-MASKED]cvv ", +" the cvv is 411111******1111\"}Bearer [SAD-MASKED]", +"}Bearer token=[SECRET-MASKED]", +null, +"s3cr3ttoken=ptok:v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA[SAD-MASKED]cvv=,password=[SECRET-MASKED]>", +null, +null, +"4111111111111111123-45-6789[1234password= cvv=[CVV-MASKED]", +null, +"password="4111111111111111:payment_id=abc12345350 mscvv=\"[CVV-MASKED]", +null, +"[SAD-MASKED]]\"Authorization: (jane@example.com123-45-6789&" +] +} diff --git a/tests/test_contrib_ottu_wallets.py b/tests/test_contrib_ottu_wallets.py new file mode 100644 index 0000000..6852572 --- /dev/null +++ b/tests/test_contrib_ottu_wallets.py @@ -0,0 +1,288 @@ +"""With Ottu's WALLET_RULES, a wallet token's payment data is `[SAD-MASKED]` +in every pack (#160054). + +An Apple Pay or Google Pay token carries the device card number and the +payment cryptogram, encrypted. It is never hashed: the whole token is the SAD +label, as a cryptogram under its own key is. It is found by its shape, not by +a key name, so an unrelated `paymentData` stays readable and the token is +masked under any key -- `paymentData`, TAP's `token_data`, MPGS's +`paymentToken` (a JSON string), Google Pay's `tokenizationData.token`, KPay's +``. + +The shapes are Ottu's, not ecsctx's: `ecsctx.contrib.ottu.masking.WALLET_RULES`, +value rules a service lists in ECSCTX_MASK_VALUE_RULES. Every test here runs +with them configured, but the last, which runs with nothing configured: core +masks no wallet token of its own accord. + +- Apple Pay (PKPaymentToken's `paymentData`): an object whose `version` is + `EC_v1` or `RSA_v1`, with the encrypted `data`. `signature` and `header` + are useless without it, so the whole object is the label. +- Google Pay (the payment method token): an object, or a JSON string, whose + `protocolVersion` is `ECv1`, `ECv2` or `ECv2SigningOnly`, with a + `signedMessage`. + +Every test runs with a keyset and without one. +""" + +import json +import logging + +import pytest + +from ecsctx.contrib.net import loggable_request_body, redact_body +from ecsctx.contrib.ottu.masking import WALLET_RULES +from ecsctx.masking.config import configure_masking_packs, configure_masking_value_rules +from ecsctx.masking.filters import MaskPIIFilter +from ecsctx.masking.patterns import ALL_PACKS, mask_by_patterns, rules_for +from ecsctx.pii import configure_pii +from ecsctx.processors import mask_sensitive_data + +SAD = "[SAD-MASKED]" + +APPLE_PAY = { + "data": "ZGF0YWJsb2JkYXRhYmxvYg==", + "signature": "c2lnbmF0dXJlLWJsb2I=", + "header": { + "publicKeyHash": "cGtoYXNo", + "ephemeralPublicKey": "ZXBoZW1lcmFsLWtleQ==", + "transactionId": "7134e7d22988391fa183a61a191ae14c", + }, + "version": "EC_v1", +} +APPLE_PAY_RSA = { + "data": "cnNhLWRhdGE=", + "signature": "c2ln", + "header": {"wrappedKey": "d3JhcHBlZA==", "publicKeyHash": "cGto", "transactionId": "abc"}, + "version": "RSA_v1", +} +PK_PAYMENT_TOKEN = { + "paymentData": APPLE_PAY, + "paymentMethod": {"displayName": "Visa 1234", "network": "Visa", "type": "debit"}, + "transactionIdentifier": "7134E7D22988391FA183A61A191AE14C", +} +GOOGLE_PAY = { + "signature": "MEQCIGZha2Utc2lnbmF0dXJl", + "intermediateSigningKey": { + "signedKey": json.dumps({"keyValue": "a2V5", "keyExpiration": "1700000000000"}), + "signatures": ["MEYCIQDmYWtl"], + }, + "protocolVersion": "ECv2", + "signedMessage": json.dumps({"encryptedMessage": "ZW5j", "ephemeralPublicKey": "ZXBr", "tag": "dGFn"}), +} +GOOGLE_PAY_V1 = {"signature": "MEQCIA==", "protocolVersion": "ECv1", "signedMessage": '{"encryptedMessage":"ZW5j"}'} +NOT_A_WALLET = {"amount": "10.000", "currency": "KWD"} + +PACKS = [["default"], sorted(ALL_PACKS)] + + +@pytest.fixture(autouse=True, params=["keyset", "no-keyset"]) +def mode(request, token_keyset_path): + if request.param == "keyset": + configure_pii(token_keyset_path=token_keyset_path, env="test") + return request.param + + +@pytest.fixture(autouse=True, params=PACKS, ids=["default", "every-pack"]) +def packs(request): + configure_masking_packs(request.param) + return frozenset(request.param) + + +@pytest.fixture(autouse=True) +def wallet_rules(): + configure_masking_value_rules(WALLET_RULES) + + +def _walk(event: dict) -> dict: + return mask_sensitive_data(None, "info", json.loads(json.dumps(event))) + + +def _text(text: str, packs: frozenset[str]) -> str: + return mask_by_patterns(text, rules_for(packs | {"default"})) + + +def _message(msg: str, *args) -> str: + record = logging.LogRecord("t", logging.INFO, __file__, 0, msg, args, None) + MaskPIIFilter().filter(record) + return record.getMessage() + + +def _holds_no_wallet_data(text: str) -> None: + for secret in ("ZGF0YWJsb2JkYXRhYmxvYg==", "c2lnbmF0dXJlLWJsb2I=", "ZXBoZW1lcmFsLWtleQ==", "MEQCIGZha2Utc2lnbmF0dXJl"): + assert secret not in text + + +class TestTheKeyWalk: + @pytest.mark.parametrize("key", ["paymentData", "token_data", "apple_pay", "payload", "udf9", "token", "card"]) + def test_an_apple_pay_token_under_any_key_is_the_label(self, key): + assert _walk({key: APPLE_PAY}) == {key: SAD} + + def test_the_rsa_version_too(self): + assert _walk({"paymentData": APPLE_PAY_RSA}) == {"paymentData": SAD} + + def test_a_pk_payment_token_keeps_what_is_not_the_payment_data(self): + masked = _walk({"apple_pay_payload": PK_PAYMENT_TOKEN})["apple_pay_payload"] + assert masked["paymentData"] == SAD + assert masked["paymentMethod"] == PK_PAYMENT_TOKEN["paymentMethod"] + + @pytest.mark.parametrize("token", [GOOGLE_PAY, GOOGLE_PAY_V1]) + def test_a_google_pay_token_object_is_the_label(self, token): + assert _walk({"google_pay": token}) == {"google_pay": SAD} + + @pytest.mark.parametrize("key", ["token", "paymentToken", "payload"]) + @pytest.mark.parametrize("token", [APPLE_PAY, GOOGLE_PAY]) + def test_a_token_as_a_json_string_is_the_label(self, key, token): + # MPGS's paymentToken, Google Pay's tokenizationData.token: a JSON + # string under a credential key was hashed. + assert _walk({key: json.dumps(token)}) == {key: SAD} + + def test_the_mpgs_device_payment_body(self): + body = { + "sourceOfFunds": {"provided": {"card": {"devicePayment": {"paymentToken": json.dumps(APPLE_PAY)}}}}, + "order": {"amount": "10.000", "currency": "KWD"}, + } + masked = _walk({"payload": body})["payload"] + assert masked["sourceOfFunds"]["provided"]["card"]["devicePayment"]["paymentToken"] == SAD + assert masked["order"] == body["order"] + + def test_the_google_pay_payment_data(self): + payment_data = { + "apiVersion": 2, + "paymentMethodData": { + "type": "CARD", + "description": "Visa 1234", + "tokenizationData": {"type": "PAYMENT_GATEWAY", "token": json.dumps(GOOGLE_PAY)}, + }, + } + masked = _walk({"google_pay_payload": payment_data})["google_pay_payload"] + assert masked["paymentMethodData"]["tokenizationData"] == {"type": "PAYMENT_GATEWAY", "token": SAD} + assert masked["paymentMethodData"]["description"] == "Visa 1234" + + def test_a_token_in_a_list(self): + assert _walk({"tokens": [APPLE_PAY, json.dumps(GOOGLE_PAY)]}) == {"tokens": [SAD, SAD]} + + def test_a_wrapper_as_a_string_under_a_credential_key_is_the_label(self): + # Never hashed, even with the token inside something else. + assert _walk({"token": json.dumps(PK_PAYMENT_TOKEN)}) == {"token": SAD} + + def test_a_wrapper_as_a_string_under_any_other_key_keeps_its_shape(self): + masked = json.loads(_walk({"body": json.dumps(PK_PAYMENT_TOKEN)})["body"]) + assert masked["paymentData"] == SAD + assert masked["paymentMethod"] == PK_PAYMENT_TOKEN["paymentMethod"] + + @pytest.mark.parametrize( + "value", + [ + NOT_A_WALLET, + {"version": "EC_v1", "header": {"publicKeyHash": "cGto"}}, + {"version": "2", "data": "abc"}, + {"protocolVersion": "ECv2"}, + {"protocolVersion": "v3", "signedMessage": "x"}, + ], + ) + def test_anything_else_under_payment_data_is_read_as_before(self, value): + assert _walk({"paymentData": value}) == {"paymentData": value} + + def test_a_record_with_args(self): + assert _message("apple pay token %s", json.dumps(APPLE_PAY)) == f"apple pay token {SAD}" + assert _message("token %(t)s", {"t": APPLE_PAY}) == f"token {SAD}" + + +class TestText: + @pytest.mark.parametrize("token", [APPLE_PAY, APPLE_PAY_RSA, GOOGLE_PAY, GOOGLE_PAY_V1]) + def test_a_token_in_a_message_is_the_label(self, token, packs): + assert _text(f"apple pay token {json.dumps(token)} received", packs) == f"apple pay token {SAD} received" + + def test_compact_json_and_a_python_repr_too(self, packs): + compact = json.dumps(APPLE_PAY, separators=(",", ":")) + assert _text(f"token={compact}", packs) == f"token={SAD}" + assert _text(f"token {APPLE_PAY!r}", packs) == f"token {SAD}" + + def test_in_json_text_the_label_is_a_json_string(self, packs): + text = "sent " + json.dumps({"paymentData": APPLE_PAY, "amount": "10.000"}) + masked = _text(text, packs) + assert masked == 'sent {"paymentData": "[SAD-MASKED]", "amount": "10.000"}' + assert json.loads(masked[5:]) + + def test_a_token_as_a_json_string_in_json_text(self, packs): + text = "sent " + json.dumps({"paymentToken": json.dumps(GOOGLE_PAY), "amount": "10.000"}) + masked = _text(text, packs) + assert masked == 'sent {"paymentToken": "[SAD-MASKED]", "amount": "10.000"}' + + def test_a_message_that_is_json(self): + masked = _walk({"event": json.dumps({"paymentData": APPLE_PAY})})["event"] + assert json.loads(masked) == {"paymentData": SAD} + + def test_the_processor_masks_it_in_any_string(self): + event = {"event": f"apple pay token {json.dumps(APPLE_PAY)}", "note": json.dumps(GOOGLE_PAY)} + assert _walk(event) == {"event": f"apple pay token {SAD}", "note": SAD} + + def test_an_unrelated_payment_data_stays_readable(self, packs): + text = "paymentData " + json.dumps({"paymentData": NOT_A_WALLET}) + assert _text(text, packs) == text + + def test_masking_twice_masks_once(self, packs): + once = _text(f"token {json.dumps(PK_PAYMENT_TOKEN)}", packs) + _holds_no_wallet_data(once) + assert "ptok:v1:" not in once + assert _text(once, packs) == once + + +class TestBodies: + def test_redact_body(self): + masked = redact_body(json.dumps({"paymentData": APPLE_PAY, "amount": "10.000"})) + assert json.loads(masked) == {"paymentData": SAD, "amount": "10.000"} + assert redact_body(masked) == masked + + def test_redact_body_on_a_token_as_a_json_string(self): + masked = redact_body(json.dumps({"paymentToken": json.dumps(APPLE_PAY)})) + assert json.loads(masked) == {"paymentToken": SAD} + + def test_loggable_request_body(self): + masked = loggable_request_body(None, {"paymentToken": json.dumps(GOOGLE_PAY), "amount": "10.000"}) + assert json.loads(masked) == {"paymentToken": SAD, "amount": "10.000"} + + def test_kpay_udf9_through_redact_body_and_the_processor(self, packs): + body = ( + "TRANPORTAL123S3cretPassw0rd1" + f"10.000TRK1{json.dumps(PK_PAYMENT_TOKEN)}" + ) + for masked in (redact_body(body), _walk({"http": {"body": body}})["http"]["body"]): + _holds_no_wallet_data(masked) + udf9 = masked.split("")[1].split("")[0] + assert json.loads(udf9)["paymentData"] == SAD + assert json.loads(udf9)["paymentMethod"] == PK_PAYMENT_TOKEN["paymentMethod"] + + def test_kpay_udf9_holding_the_payment_data_itself(self): + # What KPay's client sends: udf9 is the paymentData object, unwrapped. + body = f"7134E7D2{json.dumps(APPLE_PAY)}10.000" + expected = f"7134E7D2{SAD}10.000" + assert redact_body(body) == expected + assert _walk({"body": body})["body"] == expected + + def test_an_entity_encoded_token_in_an_element(self): + encoded = json.dumps(APPLE_PAY).replace('"', """) + assert redact_body(f"{encoded}") == f"{SAD}" + + def test_a_credential_element_holding_a_token_is_the_label_not_its_hash(self): + assert redact_body(f"{json.dumps(APPLE_PAY)}") == f"{SAD}" + + +class TestNothingConfigured: + """Core names no wallet shape: without WALLET_RULES in + ECSCTX_MASK_VALUE_RULES, a wallet token is not the label -- the opt-in is + explicit.""" + + @pytest.fixture(autouse=True) + def no_value_rules(self, wallet_rules): + configure_masking_value_rules(()) + + def test_a_token_is_not_labelled_by_core(self, packs): + assert _walk({"paymentData": APPLE_PAY}) != {"paymentData": SAD} + assert SAD not in _text(f"apple pay token {json.dumps(APPLE_PAY)} received", packs) + assert SAD not in redact_body(json.dumps({"paymentData": APPLE_PAY})) + assert SAD not in _walk({"google_pay": json.dumps(GOOGLE_PAY)})["google_pay"] + + def test_the_setting_opts_in(self): + configure_masking_value_rules(["ecsctx.contrib.ottu.masking.WALLET_RULES"]) + assert _walk({"paymentData": APPLE_PAY}) == {"paymentData": SAD} diff --git a/tests/test_masking_bare_pan.py b/tests/test_masking_bare_pan.py index c6c08cf..37fa7ce 100644 --- a/tests/test_masking_bare_pan.py +++ b/tests/test_masking_bare_pan.py @@ -11,12 +11,12 @@ 1. `already_masked()` recognised a masked value by the literal `-MASKED:` / `-MASKED]`, keeping a re-passed value from being masked a second time; 2. `mask_card_value` recognised its own output via `_SINGLE_MARKER`; -3. `_text_has_card_context` — added in this same release to stop the - standalone-CVV rule eating response codes — read the word "CARD" out of the - marker. Once the card rule fires the digit run is gone, so the marker was the - only card context left in the text. Miss this one and a CVV beside a masked - PAN silently stops being masked, which is a PCI leak, not a regression in - formatting. +3. the bare CVV rule -- narrowed in this same release to stop it eating + response codes -- read the word "CARD" out of the marker. Once the card rule + fires the digit run is gone, so the truncation is the card a CVV follows + (since 0.15.6 the rule reads only a CVV beside one, #160054). Miss this one + and a CVV beside a masked PAN silently stops being masked, which is a PCI + leak, not a regression in formatting. """ import pytest @@ -148,23 +148,25 @@ def test_a_short_partial_is_shown(self, mask): class TestTheCvvBesideAMaskedPanStillMasks: - """The leak this change opens if `_text_has_card_context` is not taught the - bare shape. The card rule runs before the standalone CVV rule, so by the time the CVV rule looks - at the text the PAN is already truncated and `_CARD_SHAPE` no longer - matches — the truncation itself is the only card context left. + """The leak this change opens if the bare CVV rule is not taught the bare + shape. The card rule runs before it, so by the time it looks at the text + the PAN is already truncated: the truncation is the card a CVV follows. + Since 0.15.6 only a CVV beside it (#160054): a word between them makes + the digits a reference, not a CVV. """ def test_a_cvv_after_a_pan_in_one_string(self): - # A word between them on purpose: the context the CVV rule finds is - # the truncation, not a digit run beside the CVV. - out = mask_by_all_patterns(f"{PAN} ref 123") - assert TRUNCATED in out - assert "[CVV-MASKED]" in out + # `|`, which never joins card digits: a space would make this PAN, + # which fails Luhn, and the 123 one 19-digit number. + out = mask_by_all_patterns(f"{PAN}|123") + assert out == f"{TRUNCATED}|[CVV-MASKED]" def test_a_cvv_after_an_already_truncated_pan(self): """The second pass over a document the first pass already masked.""" - out = mask_by_all_patterns(f"{TRUNCATED} ref 123") - assert "[CVV-MASKED]" in out + assert mask_by_all_patterns(f"{TRUNCATED} 123") == f"{TRUNCATED} [CVV-MASKED]" + + def test_a_reference_after_the_pan_is_not_a_cvv(self): + assert mask_by_all_patterns(f"{PAN}| ref 123") == f"{TRUNCATED}| ref 123" def test_a_status_code_with_no_card_anywhere_still_survives(self): """The other half of the same precondition, which must not regress.""" diff --git a/tests/test_masking_cvv_bare.py b/tests/test_masking_cvv_bare.py new file mode 100644 index 0000000..66644c6 --- /dev/null +++ b/tests/test_masking_cvv_bare.py @@ -0,0 +1,118 @@ +"""A bare 3-4 digit run is a CVV only beside a card number or a CVV word (#160054). + +The `pci` pack's bare-digit rule masked every 3-4 digit run in a line that +mentioned a card anywhere: `card list returned 200 in 350 ms` became +`card list returned [CVV-MASKED] in [CVV-MASKED] ms`, and `declined with code +051` lost its code. A CVV is worth nothing without the card it belongs to, +and it is written beside it: after the card number or its truncation, +perhaps with the expiry between (`4111111111111111|12/27|123`), or right +after a word naming it (`the cvv is 123`). The keyed rules (`cvv=123`, +`CVV 123`) still mask a CVV in every pack. +""" + +import pytest + +from ecsctx.masking.filters import MaskPIIFilter +from ecsctx.masking.patterns import ALL_PACKS + +LABEL = "[CVV-MASKED]" + +MUST_MASK = [ + ("4111111111111111 123", f"411111******1111 {LABEL}"), + ("411111******1111 123", f"411111******1111 {LABEL}"), + ("4111111111111111|12/27|123", f"411111******1111|12/27|{LABEL}"), + ("pan 4111111111111111 12/27 123 ok", f"pan 411111******1111 12/27 {LABEL} ok"), + ("4111111111111111,12,27,123", f"411111******1111,12,27,{LABEL}"), + # Which of two such fields is the CVV cannot be told: neither ships. + ("4111111111111111;1227;1234", f"411111******1111;{LABEL};{LABEL}"), + ("4111111111111111 123 1227", f"411111******1111 {LABEL} {LABEL}"), + ("pan 4111111111111111 1225", f"pan 411111******1111 {LABEL}"), + ("card 4111111111111111 exp 12/27 123", f"card 411111******1111 exp 12/27 {LABEL}"), + ("card 4111111111111111 expiry: 1227 123", f"card 411111******1111 expiry: 1227 {LABEL}"), + ("4111111111111111 123 12/27", f"411111******1111 {LABEL} 12/27"), + ("4111111111111111\n123", f"411111******1111\n{LABEL}"), + ("paid with 4111111111111111 123.", f"paid with 411111******1111 {LABEL}."), + ("411111******1111: 123", f"411111******1111: {LABEL}"), + # Below 15 digits a PAN keeps its last four only. + ("400000000002 1234", f"********0002 {LABEL}"), + ("********1111 123", f"********1111 {LABEL}"), + # A card number the card rule leaves -- glued to a word -- is still one. + ("REF4111111111111111 123", f"REF4111111111111111 {LABEL}"), + # After a word naming it, with a word or a sign between. + ("the cvv is 123", f"the cvv is {LABEL}"), + ("security code was 1234", f"security code was {LABEL}"), + ("cvv - 123 entered", f"cvv - {LABEL} entered"), + ("CVV2 # 123", f"CVV2 # {LABEL}"), + ("cvv number 123", f"cvv number {LABEL}"), + # The keyed rules, as before. + ("card 411111******1111 cvv 123", f"card 411111******1111 cvv {LABEL}"), + ("cvv=123 for 411111******1111", f"cvv={LABEL} for 411111******1111"), +] + +MUST_NOT_MASK = [ + # The lines #160054 names. + "card list returned 200 in 350 ms", + "declined with code 051", + "card 411111******1111 declined with code 051", + # HTTP statuses, durations, decline codes. + "card lookup: HTTP 200", + "Response status: 400", + "card 411111******1111 returned 404", + "card verified in 350 ms", + "card declined, code 05", + "card declined, code 91", + # Years and expiry. + "card expires 2027", + "card valid until 2027", + "card expiry 12/27", + "411111******1111 12/27", + "411111******1111 exp 12/2027", + "411111******1111 12/2027", + "411111******1111 2026-09-30 10:30", + # Amounts and counts. + "card charged 100.000 KWD", + "411111******1111 100.000 KWD", + "411111******1111 1,250.500 KWD", + "3 cards, 250 items, 1200 rows", + # A word between the card and the digits: a reference, not a CVV. + "charged card 411111******1111 ref 123", + "411111******1111 order 1234", + # Something about a CVV, not one. + "cvv check returned 200", + "cvv_required 1 of 200", + # Digit groups of a longer number the card rule leaves whole: they were + # [CVV-MASKED] while the text carried "card context" (a strict xfail). + "1123 4567 8912 34567890", + "0109 2815 9013 96245957", +] + + +@pytest.fixture +def mask(): + return MaskPIIFilter(packs=ALL_PACKS)._mask_value + + +@pytest.mark.parametrize(("text", "masked"), MUST_MASK) +def test_a_cvv_beside_a_card_or_a_cvv_word_is_masked(mask, text, masked): + assert mask(text) == masked + assert mask(masked) == masked + + +@pytest.mark.parametrize("text", MUST_NOT_MASK) +def test_other_three_and_four_digit_runs_are_left_as_they_are(mask, text): + assert "[CVV-MASKED]" not in mask(text) + + +def test_the_card_line_keeps_its_numbers(): + # The card is still truncated; nothing else is touched. + assert MaskPIIFilter(packs=ALL_PACKS)._mask_value("card 4111111111111111 listed: 200 in 350 ms") == ( + "card 411111******1111 listed: 200 in 350 ms" + ) + + +def test_the_rule_still_never_reads_a_whole_field_value(): + assert MaskPIIFilter(packs=ALL_PACKS)._mask_value({"code": "051", "status": "200"}) == {"code": "051", "status": "200"} + + +def test_the_default_pack_has_no_bare_digit_rule(): + assert MaskPIIFilter(packs=("default",))._mask_value("411111******1111 123") == "411111******1111 123" diff --git a/tests/test_masking_filter.py b/tests/test_masking_filter.py index c34c336..186e818 100644 --- a/tests/test_masking_filter.py +++ b/tests/test_masking_filter.py @@ -387,7 +387,9 @@ def test_masks_pem_key_blocks(label, sample, expected): # key examples with api prefix ("cred-apikey", "apikey abcd1234", "apikey [SECRET-MASKED]"), ("cred-api_key", "api_key abcd1234", "api_key [SECRET-MASKED]"), - ("cred-api-key", "api-key abcd1234", "api-key [SECRET-MASKED]"), + # Ottu's `API-Key` scheme: it and the key are one value, as after + # Authorization (#160054). + ("cred-api-key", "api-key abcd1234", "[SECRET-MASKED]"), # sensitive *_key compounds ("cred-secret-key", "secret-key abcd1234", "secret-key [SECRET-MASKED]"), ("cred-private-key", "private-key abcd1234", "private-key [SECRET-MASKED]"), @@ -432,7 +434,7 @@ def test_masks_pem_key_blocks(label, sample, expected): ( "auth-interpolated-in-message", f"Authentication failed with key 'API-Key {_HEX}'", - "Authentication failed with key 'API-Key [SECRET-MASKED]'", + "Authentication failed with key '[SECRET-MASKED]'", ), # common OAuth/API field names ("access_token-kv", "access_token=abcd1234efgh5678", "access_token=[SECRET-MASKED]"), @@ -441,7 +443,7 @@ def test_masks_pem_key_blocks(label, sample, expected): ("private_key-kv", "private_key=abcd1234efgh5678", "private_key=[SECRET-MASKED]"), ("secret_key-kv", "secret_key=abcd1234efgh5678", "secret_key=[SECRET-MASKED]"), ("auth-basic-equals", f"basic= {_HEX}", "basic= [SECRET-MASKED]"), - ("auth-api-key-value", f"API-Key {_HEX}", "API-Key [SECRET-MASKED]"), + ("auth-api-key-value", f"API-Key {_HEX}", "[SECRET-MASKED]"), ("auth-basic-value", "Basic dXNlcjpwYXNzd29yZA==", "Basic [SECRET-MASKED]"), # A quoted key whose value is a bare literal: text that is JSON (or a # Python repr) must stay parseable, and a null/boolean holds no secret. @@ -839,8 +841,8 @@ def test_masks_jwt(label, sample, expected): ("card-20d-other-space-luhn-reading", "112345 678912 34567891", "112345 **********7891"), ("card-20d-other-dash-luhn-reading", "1123-4567-8912-34567891", "1123-4567-********7891"), ("card-20d-9-space-4x4-luhn-reading", "9123 4567 8912 34567891", "912345**********7891"), - # The CVV rule still claims the two groups the card reading leaves. - ("card-20d-other-space-4x4-luhn-reading", "1123 4567 8912 34567891", "[CVV-MASKED] [CVV-MASKED] ********7891"), + # The two groups the card reading leaves are no CVV: none follows a card. + ("card-20d-other-space-4x4-luhn-reading", "1123 4567 8912 34567891", "1123 4567 ********7891"), ] @@ -944,11 +946,14 @@ def test_dict_key_value_masking(label, sample, expected): # guard leaves prose like "Basic authentication" untouched. # --------------------------------------------------------------------------- NOT_MASKED = [ - # Promoted out of OVER_MASKED_BECAUSE_OF_CVV: an 11-digit run is too short - # for the card rules AND carries no card context, so the standalone-CVV - # rule no longer claims its 4-digit groups. + # Promoted out of OVER_MASKED_BECAUSE_OF_CVV: the bare CVV rule reads a + # 3-4 digit group only beside a card number or a CVV word, and none of + # these groups follows one -- an 11-digit run is too short for the card + # rules, and a 20-digit one with no Luhn-valid reading is left whole. ("card-11d-9-space", "9123 4567 891"), ("card-11d-other-space", "1123 4567 891"), + ("card-20d-space-no-luhn-reading-in-groups", "1123 4567 8912 34567890"), + ("card-20d-other-space-no-luhn-reading-in-groups", "0109 2815 9013 96245957"), ("cache-key", "cache_key=user_profile_v2"), ("sort-key", "sort_key=created_at_desc"), ("primary-key", "primary_key=customer_00042"), @@ -998,41 +1003,6 @@ def test_does_not_over_mask(label, sample): assert _mask(sample) == sample -# --------------------------------------------------------------------------- -# Space-cascade bug (narrowed, not closed): the standalone-CVV rule — the -# loosest rule in the file, any bare 3-4 digit group — claims the 4-digit -# groups of a space-separated digit run that the card rules correctly ignored -# for being outside the 12-19 range. `expected` is the intended output -# (untouched), not what ships today. Strict xfail, so fixing it turns these -# into XPASS failures and forces promotion into NOT_MASKED. -# -# The 11-digit rows have been promoted: the rule now needs card context in the -# text, and 11 digits are too few for _CARD_SHAPE to supply it. The 20-digit -# rows stay — their first 12 digits ARE card-shaped, so the text does carry -# card context and the rule is entitled to look at the groups. Closing those -# needs the card rules to claim the whole run first, which is a change to -# the card rule, not to the CVV rule. The rows here have no Luhn-valid reading of -# 12-19 digits; the earlier ones had one, and are truncated as cards now -# (CARD_NUMBER_CASES). -# -# The in-range 4-4-4-4 rows this list carried in the ported source are no -# longer affected — truncated-PAN masking claims the whole run before the -# CVV rule can see the groups — and now live in CARD_NUMBER_CASES. -# --------------------------------------------------------------------------- -OVER_MASKED_BECAUSE_OF_CVV = [ - ("card-20d-space-no-luhn-reading", "1123 4567 8912 34567890"), - ("card-20d-other-space-no-luhn-reading", "0109 2815 9013 96245957"), -] - - -@pytest.mark.xfail(strict=True, reason="space-cascade: card-shaped text lets the CVV rule see its groups") -@pytest.mark.parametrize( - "label,sample", OVER_MASKED_BECAUSE_OF_CVV, ids=[g[0] for g in OVER_MASKED_BECAUSE_OF_CVV] -) -def test_over_masked_because_of_cvv(label, sample): - assert _mask(sample) == sample - - # --------------------------------------------------------------------------- # Accepted leaks: unlike NOT_MASKED above (values that aren't sensitive, or # don't match a rule's shape at all), every case here is genuinely diff --git a/tests/test_masking_golden.py b/tests/test_masking_golden.py new file mode 100644 index 0000000..620a1ea --- /dev/null +++ b/tests/test_masking_golden.py @@ -0,0 +1,239 @@ +"""Masking's output on a fixed corpus, byte for byte (#160054). + +0.15.5 made a rich event mask 1.4-1.7x slower than 0.15.4. The work that +won the time back must not change a single output, so this corpus -- the +suite's own tables, the benchmarks' inputs and seeded fragments -- was run +through every masking path, in the default pack and with every pack, with a +fixed keyset and without one, before that work began, and its outputs were +written to golden/masking_0156.json. Every run compares against them. + +To capture them again, deliberately, when an output is meant to change: +`ECSCTX_WRITE_GOLDEN=1 pytest tests/test_masking_golden.py`. +""" + +import base64 +import importlib.util +import json +import logging +import os +import random +from pathlib import Path + +from ecsctx.contrib.net import redact_body +from ecsctx.contrib.ottu.masking import WALLET_RULES +from ecsctx.masking.config import configure_masking_packs, configure_masking_value_rules +from ecsctx.masking.filters import MaskPIIFilter, _Pass +from ecsctx.masking.patterns import ( + ALL_PACKS, + Rule, + mask_by_patterns, + rules_for, + scalar_rules, +) +from ecsctx.pii import _reset as _reset_pii +from ecsctx.pii import configure_pii +from ecsctx.processors import mask_sensitive_data +from tests import ( + test_contrib_ottu_wallets, + test_masking_cvv_bare, + test_masking_filter, + test_masking_secrets, +) +from tests.test_masking_xml_bodies import KPAY_BODIES + +GOLDEN = Path(__file__).parent / "golden" / "masking_0156.json" +PACKS = {"default": frozenset({"default"}), "every-pack": ALL_PACKS} + +# The benchmark's inputs (bench_mask.py, scratchpad of #160054). +BENCH_BODY = json.dumps( + { + "session_id": "21a2496f8b6c193ec61b8dd52cbe8d827b4eaa49", + "order_no": "ORD-2026-000123", + "amount": "12.500", + "currency_code": "KWD", + "customer_email": "jane.roe@example.com", + "customer_phone": "+96550000000", + "customer_first_name": "Jane", + "billing_address": {"line1": "Block 1, Street 2", "city": "Kuwait City", "country": "KW"}, + "pg_codes": ["mpgs-direct", "cs-direct", "kpay"], + "redirect_url": "https://merchant.example/return?ref=abc123&lang=en", + "webhook_url": "https://merchant.example/hooks/ottu", + "extra": {"note": "gift wrap", "items": [{"sku": f"SKU-{i}", "qty": i} for i in range(10)]}, + "card_acceptance_criteria": {"min_expiry_time": 30}, + "three_ds": {"directory_server_transaction_id": "3d85c5d3-8412-4f1d-9e26-4a0f00123456"}, + } +) +BENCH_EVENT = { + "event": "api request received: POST /b/checkout/v1/pymt-txn/", + "payload": json.loads(BENCH_BODY), + "http": { + "request": { + "body": BENCH_BODY, + "headers": {"authorization": "Api-Key abcDEF123.456ghiJKL", "user-agent": "Mozilla/5.0"}, + } + }, + "url": {"full": "https://jade.ottu.dev/b/checkout/v1/pymt-txn/?utm=1"}, +} +BENCH_TEXT = ( + "gateway replied 200 for session 21a2496f8b6c193ec61b8dd52cbe8d827b4eaa49 order ORD-2026-000123 " + "amount 12.500 KWD at https://ap-gateway.mastercard.com/api/rest/version/100/merchant/TEST123/order/77 " + "trace 0ca2002f-f3f9-4985-ba8f-eb80e08f37b5 took 1234 ms; result=SUCCESS gatewayCode=APPROVED " +) * 8 + +# Seeded fragments: shapes no table holds, the same on every run. +FRAGMENTS = ( + "password=", "token=", '"token": "', "Bearer ", "Authorization: ", "cvv=", "cvv ", "the cvv is ", "", + "", "", "", "", "", """, "&", "4111111111111111", "411111******1111", + "12/27", "123", "1234", "200", "350 ms", "s3cr3t", "abc123", "jane@example.com", "+96550000000", "https://u:p@h/", + "https://4111111111111111@h.example/", "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.c2ln", "GB33BUKB20201555555555", + "123-45-6789", "payment_id=abc12345", "[SECRET-MASKED]", "ptok:v1:" + "A" * 43, " ", "|", ",", ":", "'", '"', "{", + "}", "[", "]", "(", ")", "\\", "-----BEGIN RSA PRIVATE KEY-----\nMIIB\n-----END RSA PRIVATE KEY-----", + json.dumps(test_contrib_ottu_wallets.APPLE_PAY), json.dumps(json.dumps(test_contrib_ottu_wallets.GOOGLE_PAY)), +) + + +def _bench_fixtures() -> list[dict]: + path = Path(__file__).parent.parent / "scripts" / "bench_masking.py" + spec = importlib.util.spec_from_file_location("_bench_masking", path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return [event for event, _packs in module.FIXTURES.values()] + + +def _texts() -> list[str]: + texts: list[str] = [] + for table in ( + test_masking_filter.PEM_MASKED_CASES, + test_masking_filter.CREDENTIAL_MASKED_CASES, + test_masking_filter.CVV_KEYWORD_CASES, + test_masking_filter.PAYMENT_ID_QUOTE_CASES, + test_masking_filter.IBAN_MASKED_CASES, + test_masking_filter.PHONE_MASKED_CASES, + test_masking_filter.EMAIL_MASKED_CASES, + test_masking_filter.JWT_MASKED_CASES, + test_masking_filter.CARD_NUMBER_CASES, + test_masking_filter.SSN_MASKED_CASES, + test_masking_filter.NOT_MASKED, + test_masking_filter.ACCEPTED_LEAK_CASES, + ): + texts += [case[1] for case in table if isinstance(case[1], str)] + texts += test_masking_secrets.PAN_SHAPES + texts += [text for text, *_ in test_masking_secrets.USERINFO_TEXTS] + texts += [template % "s3cr3t-Hunter2" for template in test_masking_secrets.CREDENTIAL_TEXTS] + texts += [text for text, _masked in test_masking_cvv_bare.MUST_MASK] + texts += test_masking_cvv_bare.MUST_NOT_MASK + texts += [body.replace("{password}", "S3cretPassw0rd") for body in KPAY_BODIES.values()] + texts += [BENCH_BODY, BENCH_TEXT] + rng = random.Random(160054) + texts += ["".join(rng.choice(FRAGMENTS) for _ in range(rng.randint(1, 10))) for _ in range(300)] + return list(dict.fromkeys(texts)) + + +def _events() -> list[dict]: + events = [BENCH_EVENT, *_bench_fixtures()] + events += [case[1] for case in test_masking_filter.DICT_KEY_VALUE_MASKING_CASES] + events.append( + {"payload": {"paymentData": test_contrib_ottu_wallets.APPLE_PAY, "note": json.dumps(test_contrib_ottu_wallets.GOOGLE_PAY)}} + ) + events += [ + {"http": {"request": {"body": {"content": body.replace("{password}", "S3cretPassw0rd")}}}} + for body in KPAY_BODIES.values() + ] + events += [{"event": text} for text in _texts()[:200]] + return events + + +RECORDS = [ + ("user %s paid %s KWD for %s", ("bob", "12.500", "ORD-1")), + ("cvv=%s", ("123",)), + ("card %s %s", ("4111111111111111", "123")), + ("password=%(pw)s", ({"pw": ["a", "b"]},)), + ("paid %.f KWD by %d", (10.5, 5551234567)), + ("sent to %s", ("jane@example.com",)), + ("apple pay token %s", (json.dumps(test_contrib_ottu_wallets.APPLE_PAY),)), + ("retried with Bearer %s", ("abc123def456",)), + ("%s", ({"card_number": "4111111111111111", "cvv": "123"},)), +] + + +def _keyset(tmp: Path) -> str: + key = base64.urlsafe_b64encode(bytes(range(32))).rstrip(b"=").decode() + keyset = { + "schema_version": 1, + "primary_kid": "golden", + "keys": {"golden": {"alg": "HMAC-SHA-256", "created_at": "2026-09-30T00:00:00Z", "key_b64": key}}, + } + path = tmp / "golden-keyset.json" + path.write_text(json.dumps(keyset)) + return str(path) + + +def _record(msg, args, packs) -> list: + record = logging.LogRecord("golden", logging.INFO, __file__, 0, msg, args, None) + MaskPIIFilter(packs=packs).filter(record) + return [str(record.msg), repr(record.args), record.getMessage()] + + +def _changed(text: str, masked: str) -> str | None: + """The output, or None where it is the text as it came.""" + return None if masked == text else masked + + +def _outputs(tmp: Path) -> dict: + # As an Ottu service runs: its wallet shapes are value rules it lists, + # no longer core's, and the corpus holds wallet tokens. + configure_masking_value_rules(WALLET_RULES) + texts, events = _texts(), _events() + outputs: dict = {} + for mode in ("no-keyset", "keyset"): + _reset_pii() + if mode == "keyset": + configure_pii(token_keyset_path=_keyset(tmp), env="golden") + outputs[f"{mode}/redact_body"] = [_changed(text, redact_body(text)) for text in texts] + for name, packs in PACKS.items(): + configure_masking_packs(packs) + rules = rules_for(packs) + prose = MaskPIIFilter(packs=packs) + masked = [mask_by_patterns(text, rules) for text in texts] + outputs[f"{mode}/{name}/text"] = [_changed(text, out) for text, out in zip(texts, masked, strict=True)] + # The filter's reading of a message, where it differs from the + # rules' own (a JSON message is walked by its keys first). + outputs[f"{mode}/{name}/prose"] = [ + _changed(out, prose._mask_value(text)) for text, out in zip(texts, masked, strict=True) + ] + outputs[f"{mode}/{name}/events"] = [ + mask_sensitive_data(None, "info", json.loads(json.dumps(event))) for event in events + ] + outputs[f"{mode}/{name}/records"] = [_record(msg, args, packs) for msg, args in RECORDS] + _reset_pii() + return outputs + + +class _NeverHashed(Rule): + def __hash__(self): + raise AssertionError("a rule was hashed") + + +def test_a_rule_set_is_cached_by_its_identity_never_hashed(): + # What 0.15.5 lost its time to: lru_cache keyed on the rule tuple hashed + # every rule, and a compiled pattern hashes its whole program -- 30 µs a + # string for the default pack, more than all the masking it guarded. + rules = tuple(_NeverHashed(*rule) for rule in rules_for(ALL_PACKS)) + assert scalar_rules(rules) is scalar_rules(rules) + assert mask_by_patterns("card 4111111111111111 123 for jane@example.com", rules) == ( + "card 411111******1111 [CVV-MASKED] for [EMAIL-MASKED]" + ) + assert MaskPIIFilter()._mask_string("password=s3cr3t", _Pass(ALL_PACKS, rules, (), frozenset()), scalar=True) + + +def test_masking_outputs_are_byte_identical_to_the_golden_file(tmp_path): + outputs = _outputs(tmp_path) + text = json.dumps(outputs, ensure_ascii=False, indent=0, sort_keys=True, separators=(",", ":")) + if os.environ.get("ECSCTX_WRITE_GOLDEN"): + GOLDEN.parent.mkdir(exist_ok=True) + GOLDEN.write_text(text + "\n", encoding="utf-8") + golden = json.loads(GOLDEN.read_text(encoding="utf-8")) + for key in sorted(golden): + for index, expected in enumerate(golden[key]): + assert outputs[key][index] == expected, (key, index) + assert text + "\n" == GOLDEN.read_text(encoding="utf-8") diff --git a/tests/test_masking_never_raises.py b/tests/test_masking_never_raises.py index 8f5372e..7108b8b 100644 --- a/tests/test_masking_never_raises.py +++ b/tests/test_masking_never_raises.py @@ -18,6 +18,8 @@ import json import logging +import pytest + from ecsctx.masking.filters import MaskPIIFilter from ecsctx.masking.patterns import ALL_PACKS from ecsctx.processors import mask_sensitive_data @@ -28,6 +30,14 @@ def __str__(self): raise ValueError("no text for you") +class Broken: + """An argument whose `__str__` fails as no rendering expects: not the + TypeError, ValueError or KeyError that `_render` reads as "does not fit".""" + + def __str__(self): + raise RuntimeError("no text for you") + + def _record(msg, args=()): return logging.LogRecord("t", logging.INFO, __file__, 0, msg, args, None) @@ -59,6 +69,31 @@ def test_the_filter_does_not_raise_on_an_argument(self): assert MaskPIIFilter(packs=ALL_PACKS).filter(record) is True assert "MASKING-FAILED" in record.getMessage() + @pytest.mark.parametrize("packs", [("default",), ALL_PACKS], ids=["default", "pci"]) + @pytest.mark.parametrize("template", ["paid by %s", "cvv=%s", "card %s %s"]) + def test_an_argument_whose_str_raises_runtime_error_is_the_marker(self, template, packs): + args = (Broken(),) * template.count("%s") + record = _record(template, args) + assert MaskPIIFilter(packs=packs).filter(record) is True + assert (record.msg, record.args) == ("[MASKING-FAILED: RuntimeError]", ()) + assert record.getMessage() == "[MASKING-FAILED: RuntimeError]" + + def test_a_logger_call_with_it_logs_the_marker(self, logging_state): + lines = [] + + class Lines(logging.Handler): + def emit(self, record): + lines.append(record.getMessage()) + + logger = logging.getLogger("ecsctx.tests.never_raises") + logger.propagate = False + logger.setLevel(logging.INFO) + handler = Lines() + handler.addFilter(MaskPIIFilter()) + logger.addHandler(handler) + logger.info("paid by %s", Broken()) + assert lines == ["[MASKING-FAILED: RuntimeError]"] + def test_the_processor_does_not_raise(self): event = {"event": "paid", "level": "info", "payer": Unprintable(), "name": "Jane Payer"} masked = mask_sensitive_data(None, None, event) diff --git a/tests/test_masking_packs.py b/tests/test_masking_packs.py index 20d8a43..ae12556 100644 --- a/tests/test_masking_packs.py +++ b/tests/test_masking_packs.py @@ -510,6 +510,7 @@ class TestCvvScanMatchesFullScan: '"', "'", ":", "=", " ", "-", "_", ",", "{", "}", "\n", "\\", "a-", "x", "cvv", "CVV2", "cvc", "Cvv", "csc", "cav2", "cvn", "cv", "number", "security", "Security", "code", "Code", "card", "Card", "verification", "value", "vpc_", "payment", "saved", "recv", "123", "4829", "482912", + " is ", " was ", "#", " ", ) def _cvv_rules(self): @@ -521,10 +522,14 @@ def test_each_keyed_cvv_rule_scans_near_its_words(self): assert all(rule.scan is not None for rule in rules) def test_matches_on_generated_text(self): + # The keyed rules, and the bare CVV after its word (`pci`), which is + # tried near the same words. + rules = [*self._cvv_rules(), *(rule for rule in RULES if rule.name.endswith(":_cvv_after_word"))] + assert len(rules) == 4 rng = random.Random(159942) for _ in range(3000): text = "".join(rng.choice(self.FRAGMENTS) for _ in range(rng.randint(1, 14))) - for rule in self._cvv_rules(): + for rule in rules: assert rule.scan(rule.pattern, rule.repl, text) == rule.pattern.sub(rule.repl, text), (rule.name, text) diff --git a/tests/test_masking_printf.py b/tests/test_masking_printf.py new file mode 100644 index 0000000..d3a31e0 --- /dev/null +++ b/tests/test_masking_printf.py @@ -0,0 +1,137 @@ +"""The filter reads a %-style template as CPython's `%` does (#160054). + +When masking turns a number into text -- a phone number or a card number +passed as an int -- the filter renders the record from the masked arguments, +reading that number's conversion as `%s`. To find it, it counts the +template's conversions as CPython does; `%.f`, a precision with no digits, +was not read as one, so the count slipped, the `%d` meant for the masked +number met text, and the record became `[MASKING-FAILED: TypeError]`. + +Hypothesis is not a test dependency, so this is a generated table: every +flag, width, precision, length modifier and conversion, each rendered by the +filter before a number masking turns into text and compared with CPython's +own rendering of the same conversion, the masked number after it, and the +line masked whole, as the filter masks what it renders. +""" + +import itertools +import logging + +import pytest + +from ecsctx.masking.filters import MaskPIIFilter +from ecsctx.masking.patterns import mask_by_patterns, rules_for +from ecsctx.masking.tokens import mask_by_field_type +from ecsctx.pii import configure_pii + +# A number the default pack masks as text: a phone number. +PHONE = 5551234567 +# Each flag, and flags together in either order. +FLAGS = ["", "-", "0", "#", " ", "+", "-0", "0-", "- +#0"] +WIDTHS = ["", "1", "9", "*"] +PRECISIONS = ["", ".", ".0", ".2", ".*"] +LENGTHS = ["", "h", "l", "L"] +CONVERSIONS = "diouxXeEfFgGcrsa" +VALUES = {**dict.fromkeys("diouxX", 7), **dict.fromkeys("eEfFgG", 10.5), "c": "c", "r": "x", "s": "x", "a": "x"} + + +# The tables read templates, which no keyset changes: they run without one, +# the masked number its label. The templates below run in both modes. +@pytest.fixture(params=["keyset", "no-keyset"]) +def mode(request, token_keyset_path): + if request.param == "keyset": + configure_pii(token_keyset_path=token_keyset_path, env="test") + return request.param + + +def _rendered(msg, args) -> str: + record = logging.LogRecord("t", logging.INFO, __file__, 0, msg, args, None) + MaskPIIFilter(packs=("default",)).filter(record) + return record.getMessage() + + +def _masked(text: str) -> str: + """What the filter does to a line it renders: the rules, over all of it + (`+10.500000` reads as a phone number).""" + return mask_by_patterns(text, rules_for(frozenset({"default"}))) + + +def _masked_phone() -> str: + return _masked(str(PHONE)) + + +def _mask_by_key(number: int) -> str: + return mask_by_field_type(str(number), "phone") + + +def _cpython(spec: str, args): + """CPython's rendering of one conversion, or None where it refuses it.""" + try: + return spec % args + except (TypeError, ValueError): + return None + + +def _star_args(width: str, precision: str) -> tuple: + return (*((3,) if width == "*" else ()), *((2,) if precision == ".*" else ())) + + +@pytest.mark.parametrize("length", LENGTHS) +@pytest.mark.parametrize("conversion", CONVERSIONS) +def test_every_conversion_before_a_masked_number_renders_as_cpython_renders_it(conversion, length): + phone = _masked_phone() + assert phone != str(PHONE) + checked = 0 + for flags, width, precision in itertools.product(FLAGS, WIDTHS, PRECISIONS): + spec = f"%{flags}{width}{precision}{length}{conversion}" + args = (*_star_args(width, precision), VALUES[conversion]) + if (expected := _cpython(spec, args)) is None: + continue + assert _rendered(f"{spec} then %d", (*args, PHONE)) == _masked(f"{expected} then {phone}"), spec + checked += 1 + assert checked + + +@pytest.mark.parametrize("length", LENGTHS) +@pytest.mark.parametrize("conversion", CONVERSIONS) +def test_every_keyed_conversion_renders_as_cpython_renders_it(conversion, length): + # A number in a mapping is masked by its key, as in any mapping: under + # `phone` it becomes text. + phone = _mask_by_key(PHONE) + for flags, width, precision in itertools.product(FLAGS, WIDTHS[:-1], PRECISIONS[:-1]): + spec = f"%(v){flags}{width}{precision}{length}{conversion}" + args = {"v": VALUES[conversion], "phone": PHONE} + if (expected := _cpython(spec, args)) is None: + continue + assert _rendered(f"{spec} then %(phone)d", (args,)) == _masked(f"{expected} then {phone}"), spec + + +@pytest.mark.parametrize( + ("msg", "args", "expected"), + [ + # #160054: the precision with no digits. + ("paid %.f KWD by %d", (10.5, PHONE), "paid 10 KWD by {phone}"), + ("%5.f|%-*.*f|%d", (2.5, 8, 2, 1.5, PHONE), " 2|1.50 |{phone}"), + # A literal percent takes no argument; `%%%d` is one and a conversion. + ("100%% of %d", (PHONE,), "100% of {phone}"), + ("%%%d", (PHONE,), "%{phone}"), + # A key holding parentheses, as CPython reads it: to the one that + # balances the first. + ("%(a(b))s %(phone)d", ({"a(b)": "x", "phone": PHONE},), "x {keyed}"), + ], +) +def test_templates_the_old_reading_miscounted(mode, msg, args, expected): + assert _rendered(msg, args) == expected.format(phone=_masked_phone(), keyed=_mask_by_key(PHONE)) + + +@pytest.mark.parametrize( + ("msg", "args"), + [ + # CPython reads a `%` after a flag, width, key or precision as an + # unsupported conversion: nothing to render, the marker as before. + ("%5% %d", (PHONE,)), + ("%(a)% %(phone)d", ({"a": 1, "phone": PHONE},)), + ], +) +def test_a_template_cpython_refuses_is_still_the_marker(mode, msg, args): + assert _rendered(msg, args).startswith("[MASKING-FAILED: ") diff --git a/tests/test_masking_secrets.py b/tests/test_masking_secrets.py index 30f3c5f..d1d36c2 100644 --- a/tests/test_masking_secrets.py +++ b/tests/test_masking_secrets.py @@ -23,7 +23,9 @@ by ecsctx under the same keyset, never written out. """ +import dataclasses import json +import logging import re from urllib.parse import quote, unquote, urlsplit @@ -50,6 +52,9 @@ # A saved card's gateway token is sixteen digits: Luhn-valid, and not. CARD_SHAPED = ["4111111111111111", "9923960000004314"] PLACEHOLDERS = ["[REDACTED]", "[PII_REDACTED]", "*", "***", "****", "Bearer ****"] +# A card truncation after an auth scheme: the card rule's output, or another +# masker's, where a credential stood. +SCHEMED_TRUNCATIONS = ["Bearer 411111******1111", "Basic 411111******1111", "Bearer ********1111", "token 411111******1111"] @pytest.fixture(autouse=True, params=["keyset", "no-keyset"]) @@ -93,6 +98,13 @@ def test_a_truncation_is_the_label(self, value): # credential the card rule truncated shows too much of itself. assert mask_by_field_type(value, "secret") == LABEL + @pytest.mark.parametrize("value", SCHEMED_TRUNCATIONS) + def test_a_truncation_after_a_scheme_is_the_label(self, value): + # #160054: with a keyset it was hashed whole, a token of the scheme + # and the truncation. + assert mask_by_field_type(value, "secret") == LABEL + assert mask_secret(value) == LABEL + class TestMaskSecret: """A credential that reaches a log outside a mapping -- a URL, a body a @@ -146,7 +158,7 @@ def test_an_authorization_header_another_masker_starred_is_the_label(self): _TEXT_RULES = rules_for(ALL_PACKS) -# Rule 2 (a quoted key), rule 3 (`key=value`) and rule 8 (an auth scheme). +# Rule 5 (a quoted key), rule 7 (`key=value`) and rule 13 (an auth scheme). CREDENTIAL_TEXTS = [ 'sent {"password": "%s"} to the gateway', "login with password=%s failed", @@ -166,7 +178,7 @@ def test_a_secret_shaped_like_a_card_number_is_the_label(self, text, value): @pytest.mark.parametrize("value", ["[REDACTED]", "***"]) def test_a_placeholder_under_a_quoted_key_is_the_label(self, value): - # Rules 3 and 8 never take `[REDACTED]`: a value never starts with `[`, + # Rules 7 and 13 never take `[REDACTED]`: a value never starts with `[`, # which opens a label. `***` after `=` is the label too (below). assert mask_by_patterns(CREDENTIAL_TEXTS[0] % value, _TEXT_RULES) == CREDENTIAL_TEXTS[0] % LABEL @@ -571,9 +583,9 @@ def test_through_loggable_body_a_masked_value_is_left_as_written(self): class TestAFormValueRunsToItsEnd: """A form value is everything up to the next `&` or whitespace, as in - 0.15.3. Only the structure at its two ends -- a quote or an escaped one, - and `}`, `]`, `,`, `>` or `/>` at the end -- stays outside the mask; the - rest is the value, however a quote inside it reads.""" + 0.15.3, or an end tag. Only the structure at its two ends -- a quote or + an escaped one, and `}`, `]`, `,`, `>` or `/>` at the end -- stays outside + the mask; the rest is the value, however a quote inside it reads.""" @pytest.mark.parametrize( ("body", "masked"), @@ -581,7 +593,8 @@ class TestAFormValueRunsToItsEnd: ('', f''), ('error: client_secret="4111111111111111" rejected', f'error: client_secret="{LABEL}" rejected'), ('', f''), - ('x', f'x', f'` -# as its credential and hashes it again -- a token of a token, correlation -# only. Skipping `key=` there would let through a credential glued to a -# key name (`Token abc123abc123api_key=...`). Strict xfail: fixing it fails -# here, to be promoted into the test above. -@pytest.mark.parametrize( - "shape", - [ + # After a scheme word redact_body masks a form value the text rules + # left (a label with more after it, a lone quote), and the scheme + # word's bare-space rule read `password=` as its credential and + # hashed it again: a token of a token, a strict xfail until 0.15.6. 'Bearer password=[SECRET-MASKED]"x', '123Authorization: Bearer password=[SECRET-MASKED]"note=#', "x Bearer note=password=[CVV-MASKED]'", "Bearer password='", ], ) -def test_a_scheme_words_rule_reads_what_redact_body_wrote_again(shape, mode, request): - if mode == "keyset": - request.applymarker(pytest.mark.xfail(strict=True, reason="rule 8 re-reads redact_body's key=")) - +def test_the_text_rule_then_redact_body_twice_is_once(shape): def mask(text): return redact_body(mask_by_patterns(text, _TEXT_RULES)) @@ -803,6 +797,16 @@ def mask(text): assert mask(once) == once +def test_a_credential_glued_to_a_key_name_after_a_scheme_is_still_hashed(): + # Why the scheme word's rule still reads `key=`: here the digits + # before the key name are a credential of their own. + token = hmac_tokenize("x", bytes(32), "secret", "test") + text = f"Token abc123abc123api_key={token}" + masked = mask_by_patterns(text, _TEXT_RULES) + assert "abc123abc123" not in masked + assert masked == f"Token {token_or_label(f'abc123abc123api_key={token}')}" + + # A value that holds a card number among other text, and `ptok:v1:` typed # before one in a token's exact shape. HOLDS_A_PAN = "abc4111111111111111xyz" @@ -902,6 +906,73 @@ def test_with_pci_a_uuid_keeps_its_token(self, text): assert mask_by_patterns(text % A_UUID, rules_for(ALL_PACKS)) == text % _payment_id(A_UUID) +def _email(value: str) -> str: + return tokenize(value, "email") if is_configured() else "[EMAIL-MASKED]" + + +def _phone(value: str) -> str: + return tokenize(value, "phone") if is_configured() else "[PHONE-MASKED]" + + +def _message(text: str, packs) -> str: + record = logging.LogRecord("t", logging.INFO, __file__, 0, text, None, None) + MaskPIIFilter(packs=packs).filter(record) + return record.getMessage() + + +# An email or a phone number found in text, holding a card-number run. +CARD_IN_AN_EMAIL_OR_PHONE = [ + ("reply to 4111111111111111@example.com now", "reply to [EMAIL-MASKED] now", _email, "4111111111111111@example.com"), + ("reply to jane+4111111111111111@example.com now", "reply to [EMAIL-MASKED] now", _email, "jane+4111111111111111@example.com"), + # Past the 15 digits E.164 allows a phone number, as in a phone field. + ("call +4111111111111111 now", "call [PHONE-MASKED] now", _phone, "+4111111111111111"), +] + + +class TestAnEmailOrPhoneThatHoldsACardNumber: + """An email or a phone number the text rules found was hashed whatever it + held: with `pci`, `4111111111111111@example.com` was a keyed hash of an + address holding a card number, the correlation PCI DSS FAQ 1117 warns + about, where the key walk gives the label. With `pci` it is the label, as + under its key, and a phone number is judged as a phone field judges one: + a "+"-led run of up to E.164's 15 digits is the number, and keeps its + token. Without `pci` a service receives no card numbers, and each keeps + its token.""" + + @pytest.mark.parametrize(("text", "masked", "_type", "_value"), CARD_IN_AN_EMAIL_OR_PHONE) + def test_with_pci_it_is_the_label(self, text, masked, _type, _value): + assert _message(text, ALL_PACKS) == masked + assert _message(masked, ALL_PACKS) == masked + + @pytest.mark.parametrize(("text", "masked", "_type", "_value"), CARD_IN_AN_EMAIL_OR_PHONE) + def test_in_a_pci_process_a_default_filter_labels_it_too(self, text, masked, _type, _value): + configure_masking_packs(["pci"]) + assert _message(text, ("default",)) == masked + + @pytest.mark.parametrize(("text", "_masked", "type_", "value"), CARD_IN_AN_EMAIL_OR_PHONE) + def test_without_pci_it_keeps_its_token(self, text, _masked, type_, value): + assert _message(text, ("default",)) == text.replace(value, type_(value)) + + @pytest.mark.parametrize( + ("value", "type_"), + [ + ("jane.roe@example.com", _email), + ("jane+orders@example.com", _email), + ("+96550000000", _phone), + ("+44 20 7946 0958", _phone), + ("+86 138 0013 8000", _phone), + ], + ) + @pytest.mark.parametrize("packs", [("default",), tuple(sorted(ALL_PACKS))], ids=["default", "pci"]) + def test_an_ordinary_one_keeps_its_token_either_way(self, value, type_, packs): + assert _message(f"reply to {value} now", packs) == f"reply to {type_(value)} now" + + def test_under_its_key_it_reads_the_same(self): + configure_masking_packs(["pci"]) + masked = _walk({"email": "4111111111111111@example.com", "phone": "+4111111111111111", "mobile": "+44 20 7946 0958"}) + assert masked == {"email": "[EMAIL-MASKED]", "phone": "[PHONE-MASKED]", "mobile": _phone("+44 20 7946 0958")} + + @pytest.mark.parametrize("packs", [["default"], sorted(ALL_PACKS)], ids=["default", "every-pack"]) class TestATokenShapedValueThatHoldsACardNumber: """`ptok:v1:` typed before a card number, in a token's exact shape, passed @@ -1232,6 +1303,34 @@ def test_a_user_alone_before_a_host_is_still_masked_as_an_email(self): email = tokenize("john@example.com", "email") if is_configured() else "[EMAIL-MASKED]" assert mask_by_patterns("GET https://john@example.com/v1", _TEXT_RULES) == f"GET https://{email}/v1" + @pytest.mark.parametrize("rules", [_DEFAULT_RULES, _TEXT_RULES], ids=["default", "every-pack"]) + @pytest.mark.parametrize( + "user", + [ + # A saved card's gateway token, Luhn-valid and not, as redact_url + # masks it: the label. + *CARD_SHAPED, + "4111-1111-1111-1111", + # A user holding a card-number run. + "abc4111111111111111xyz", + "4111111111111111%40corp", + ], + ) + def test_a_user_alone_shaped_like_or_holding_a_card_number_is_the_label(self, rules, user): + # #160054: in free text it shipped whole, while redact_url masked it. + text = f"GET https://{user}@api.host.example/v1 failed" + masked = mask_by_patterns(text, rules) + assert masked == f"GET https://{LABEL}@api.host.example/v1 failed" + assert mask_by_patterns(masked, rules) == masked + + def test_a_card_shaped_user_reads_as_redact_url_writes_it(self): + url = f"https://{CARD_SHAPED[1]}@host.example/v1" + assert mask_by_patterns(url, _TEXT_RULES) == unquote(redact_url(url)) + + @pytest.mark.parametrize("text", ["clone ssh://git@10.0.0.5/repo.git", "GET https://u12345@gateway/x"]) + def test_any_other_user_alone_is_still_left_to_the_other_rules(self, text): + assert mask_by_patterns(text, _TEXT_RULES) == text + # Values whose card number R4 now reads as part of the credential: 0.15.4's # value class stopped at `@`, `\` or `%`, and the card rule truncated it. @@ -1243,6 +1342,165 @@ def test_a_user_alone_before_a_host_is_still_masked_as_an_email(self): ] +def _record(msg, args) -> str: + record = logging.LogRecord("t", logging.INFO, __file__, 0, msg, args, None) + MaskPIIFilter().filter(record) + return record.getMessage() + + +@dataclasses.dataclass +class _Login: + user: str + password: list + + +class TestABareApiKeyScheme: + """Ottu PG sends `Authorization: API-Key ` into Connect. Its + own filter masked a bare `API-Key ` in text; ecsctx hashed the key + alone and left the scheme word beside it -- another token than the + header's. The scheme and the key are one value, as after Authorization: + they carry the header's token, or the label.""" + + @pytest.mark.parametrize( + ("text", "value"), + [ + ("sending API-Key abc123XYZ to core", "API-Key abc123XYZ"), + ("headers={'X': 'API-Key abc123XYZ'}", "API-Key abc123XYZ"), + ("retry with Api-Key abc123XYZ", "Api-Key abc123XYZ"), + ("api-key abc123XYZ done", "api-key abc123XYZ"), + ], + ) + def test_the_scheme_and_the_key_are_one_value(self, text, value): + masked = mask_by_patterns(text, _DEFAULT_RULES) + assert masked == text.replace(value, mask_secret(value)) + assert mask_by_patterns(masked, _DEFAULT_RULES) == masked + + def test_it_carries_the_token_the_authorization_header_carries(self): + header = mask_by_patterns("Authorization: API-Key abc123XYZ", _DEFAULT_RULES) + bare = mask_by_patterns("sending API-Key abc123XYZ to core", _DEFAULT_RULES) + assert header == f"Authorization: {mask_secret('API-Key abc123XYZ')}" + assert bare == f"sending {mask_secret('API-Key abc123XYZ')} to core" + assert _walk({"Authorization": "API-Key abc123XYZ"})["Authorization"] == mask_secret("API-Key abc123XYZ") + + @pytest.mark.parametrize( + "key", + [ + # A 44-character Fernet key with no digit (about 1 in 1,550 of Ottu + # PG's), a short key, a two-character one: the scheme word is the + # evidence, not the key's shape. + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQR", + "ab1cd2", + "Zx", + ], + ) + @pytest.mark.parametrize("scheme", ["API-Key", "api-key"]) + def test_any_key_after_the_scheme_is_masked_whole(self, scheme, key): + value = f"{scheme} {key}" + masked = mask_by_patterns(f"calling core with {value} now", _DEFAULT_RULES) + assert masked == f"calling core with {mask_secret(value)} now" + assert mask_by_patterns(masked, _DEFAULT_RULES) == masked + header = mask_by_patterns(f"Authorization: {value}", _DEFAULT_RULES) + assert header == f"Authorization: {mask_secret(value)}" + assert _walk({"Authorization": value})["Authorization"] == mask_secret(value) + + def test_a_field_inside_the_value_does_not_change_its_token(self): + # The key=value rule runs after the scheme, as it does after + # `Authorization:`: both forms carry one token for the whole value. + value = "API-Key password=s3cr3tVALUE" + assert mask_by_patterns(f"sending {value} now", _DEFAULT_RULES) == f"sending {mask_secret(value)} now" + assert mask_by_patterns(f"Authorization: {value}", _DEFAULT_RULES) == f"Authorization: {mask_secret(value)}" + + def test_a_field_masking_wrote_after_the_scheme_is_not_hashed_again(self): + # What redact_body leaves after the scheme word: its token stands. + token = hmac_tokenize("s3cr3t", bytes(32), "secret", "test") + text = f"API-Key password={token}" + assert mask_by_patterns(text, _DEFAULT_RULES) == text + + @pytest.mark.parametrize("text", ["Bearer of bad news", "basic auth enabled", "Bearer abcdefghij"]) + def test_the_other_schemes_keep_their_guards(self, text): + assert mask_by_patterns(text, _DEFAULT_RULES) == text + + def test_a_header_name_ending_in_the_word_is_no_scheme(self): + # Only the scheme word itself: `X-API-Key ` names a header, and + # its key alone is masked, as before. + assert mask_by_patterns("X-API-Key abc123XYZ", _DEFAULT_RULES) == f"X-API-Key {token_or_label('abc123XYZ')}" + + +class TestAnAuthorizationHoldingATruncation: + """`Authorization: Bearer ` is the label on every path, as a + bare truncation is: a token of the scheme and the truncation stood where + no token can.""" + + @pytest.mark.parametrize("value", SCHEMED_TRUNCATIONS) + def test_every_path(self, value): + assert _walk({"Authorization": value})["Authorization"] == LABEL + assert mask_by_patterns(f"Authorization: {value}", _TEXT_RULES) == f"Authorization: {LABEL}" + assert mask_by_patterns(f"{value}", _TEXT_RULES) == ( + f"{LABEL}" + ) + + +class TestACredentialWordBeforeAContainer: + """#160054: `password=%(pw)s` rendered with a list or dict printed + `password=['a', 'b']` -- the credential rules never start a value at an + opening bracket, and the key `pw` names nothing. The whole container is + the credential's value now: its token, or the label.""" + + @pytest.mark.parametrize( + ("msg", "args", "value"), + [ + ("password=%(pw)s", ({"pw": ["a", "b"]},), "['a', 'b']"), + ("login password=%s rejected", (["s3cret", "x"],), "['s3cret', 'x']"), + ("password: %s", ({"user": "bob", "code": "x1"},), "{'user': 'bob', 'code': 'x1'}"), + ("api_key=%s", (("k1", "k2"),), "('k1', 'k2')"), + ], + ) + def test_a_rendered_container_is_masked_whole(self, msg, args, value): + rendered = msg % (args[0] if isinstance(args[0], dict) and "%(" in msg else args) + assert _record(msg, args) == rendered.replace(value, token_or_label(value)) + + @pytest.mark.parametrize( + ("text", "value"), + [ + ("password=['a', 'b'] rejected", "['a', 'b']"), + ("secret = {'k': [1, 2]} x", "{'k': [1, 2]}"), + ("token: (1, 'x')", "(1, 'x')"), + ("password=[unclosed, 'rest of the line", "[unclosed, 'rest of the line"), + ], + ) + def test_in_text(self, text, value): + masked = mask_by_patterns(text, _TEXT_RULES) + assert masked == text.replace(value, token_or_label(value)) + assert mask_by_patterns(masked, _TEXT_RULES) == masked + + def test_a_repr_keeps_its_value_a_string(self): + text = "{'user': 'bob', 'password': ['s3cret']}" + masked = token_or_label("['s3cret']") + assert mask_by_patterns(text, _TEXT_RULES) == "{'user': 'bob', 'password': '" + masked + "'}" + + @pytest.mark.parametrize( + "text", + [ + # What masking wrote: a label, a container of tokens and labels. + "password=[SECRET-MASKED] rejected", + f"password=['{hmac_tokenize('a', bytes(32), 'secret', 'test')}', '[SECRET-MASKED]', None]", + # Nothing in it. + "password=[] and token={}", + # JSON: the key walk masked it by its keys before it was written, + # the saved card under `token` its own number, brand and expiry. + '{"token": {"brand": "VISA", "number": "411111******1111", "expiry_month": "01"}}', + '{"password": ["a"]}', + ], + ) + def test_what_is_masked_already_or_json_is_left_as_it_is(self, text): + assert mask_by_patterns(text, _TEXT_RULES) == text + + def test_a_record_rendering_a_credential_field_is_masked_once(self): + masked = MaskPIIFilter()._mask_value(_Login(user="bob", password=["s3cret"])) + assert masked == f"_Login(user='bob', password=['{token_or_label('s3cret')}'])" + assert "ptok:v1:ptok" not in masked + + class TestTheCallsPacksDecide: """"`pci` in force" is the masking call's packs -- a filter built with its own `packs=`, as a PCI handler beside a default one is -- and the diff --git a/tests/test_masking_template_split.py b/tests/test_masking_template_split.py new file mode 100644 index 0000000..79356ab --- /dev/null +++ b/tests/test_masking_template_split.py @@ -0,0 +1,151 @@ +"""A value split between a record's template and its argument is masked in +the record itself (#160054). + +`logger.info("cvv=%s", "123")` shipped masked: the formatter's processor +masks the formatted line whole. But the filter masks a record's template and +its arguments apart, and `cvv=%s` and `123` hold nothing to mask on their +own, so after it `record.getMessage()` still read `cvv=123` -- what a handler +that never calls `format()` prints, and what Sentry's logging integration and +`handleError` read. A record whose rendering masks differently from its masked +arguments rendered is now rendered, masked whole, its arguments `()`. One +where nothing spans the two keeps its template and arguments, as before. +""" + +import logging + +import pytest + +from ecsctx.masking.filters import MaskPIIFilter, _may_join +from ecsctx.masking.patterns import ALL_PACKS +from ecsctx.pii import configure_pii, is_configured, tokenize + + +@pytest.fixture(autouse=True, params=["keyset", "no-keyset"]) +def mode(request, token_keyset_path): + if request.param == "keyset": + configure_pii(token_keyset_path=token_keyset_path, env="test") + return request.param + + +def secret(value: str) -> str: + return tokenize(value, "secret") if is_configured() else "[SECRET-MASKED]" + + +def _filtered(msg, args, packs=None): + record = logging.LogRecord("t", logging.INFO, __file__, 0, msg, args, None) + MaskPIIFilter(packs=packs).filter(record) + return record + + +class _Lines(logging.Handler): + """A handler that never calls format(): it reads the record itself.""" + + def __init__(self): + super().__init__() + self.lines = [] + + def emit(self, record): + self.lines.append(record.getMessage()) + + +class TestAValueSplitBetweenTemplateAndArgument: + @pytest.mark.parametrize( + ("msg", "args", "packs", "expected"), + [ + ("cvv=%s", ("123",), None, "cvv=[CVV-MASKED]"), + ("declined: cvv %s rejected", ("1234",), None, "declined: cvv [CVV-MASKED] rejected"), + ("the cvv is %s", ("123",), ALL_PACKS, "the cvv is [CVV-MASKED]"), + ("card %s %s", ("4111111111111111", "123"), ALL_PACKS, "card 411111******1111 [CVV-MASKED]"), + ('{"card_number": "%s"}', ("5123450000000007",), None, '{"card_number": "512345******0007"}'), + ("%s", ("5123450000000007",), None, "512345******0007"), + ], + ) + def test_the_record_renders_masked(self, msg, args, packs, expected): + record = _filtered(msg, args, packs) + assert record.getMessage() == expected + assert record.args == () + + def test_a_credential_after_its_scheme(self): + record = _filtered("retried with Bearer %s", ("abc123def456",)) + assert record.getMessage() == f"retried with Bearer {secret('abc123def456')}" + + def test_a_handler_that_never_formats_prints_it_masked(self, logging_state): + logger = logging.getLogger("ecsctx.tests.template_split") + logger.setLevel(logging.INFO) + logger.propagate = False + handler = _Lines() + handler.addFilter(MaskPIIFilter()) + logger.addHandler(handler) + logger.info("cvv=%s", "123") + assert handler.lines == ["cvv=[CVV-MASKED]"] + + +class TestNothingSpansTheTwo: + def test_the_template_and_arguments_stay(self): + record = _filtered("user %s paid %s KWD", ("bob", "12.500")) + assert (record.msg, record.args) == ("user %s paid %s KWD", ("bob", "12.500")) + + def test_an_argument_masked_on_its_own_keeps_the_template(self): + record = _filtered("sent to %s", ("jane@example.com",)) + assert record.msg == "sent to %s" + assert "jane@example.com" not in record.getMessage() + + def test_a_mapping_keeps_them_too(self): + record = _filtered("paid %(amount)s by %(user)s", ({"amount": "12.500", "user": "bob"},)) + assert (record.msg, record.args) == ("paid %(amount)s by %(user)s", {"amount": "12.500", "user": "bob"}) + + +class TestOnlyATemplateThatCanJoinIsMaskedAgain: + """A record's rendering is masked a second time only where a placeholder + stands beside what a content rule reads across it: a key or scheme word + before it (`cvv=%s`, `Bearer %s`, `the cvv is %s`), structure beside it + (`=`, `:`, `@`, a quote, a bracket, digits), or another placeholder + (`card %s %s`, a card number and its CVV). Between plain words + (`user %s paid %s KWD`) nothing can join, and a second pass over every + record with arguments cost each one its rendering's masking again.""" + + @pytest.mark.parametrize( + "template", + [ + "cvv=%s", + "declined: cvv %s rejected", + "the cvv is %s", + "card %s %s", + "retried with Bearer %s", + 'sent {"password": "%s"}', + "connecting to postgresql://%s:%s@db/app", + "reply to %s@example.com", + "%s|%s|%s", + "pan %s", + "transaction_id %s", + "%s", + "call +965%s", + "ref %s4111", + "%s abc123def456", + "paid %s, %s", + ], + ) + def test_a_template_that_can_join_is(self, template): + assert _may_join(template) + + @pytest.mark.parametrize( + "template", + [ + "user %s paid %s KWD for %s", + "%s", + "gateway replied %s for session %s", + "retrying in %d seconds", + "sent to %s.", + "paid %(amount)s by %(user)s", + "100%% of %s done", + ], + ) + def test_one_between_plain_words_is_not(self, template): + assert not _may_join(template) + + def test_a_plain_template_still_renders_what_no_longer_fits(self): + # A number masking turned into text meets a `%d`: rendered, never a + # record getMessage() raises on. + record = _filtered("paid by %d", (5551234567,)) + assert record.args == () + assert "5551234567" not in record.getMessage() diff --git a/tests/test_masking_value_rules.py b/tests/test_masking_value_rules.py new file mode 100644 index 0000000..fe89baa --- /dev/null +++ b/tests/test_masking_value_rules.py @@ -0,0 +1,185 @@ +"""Value rules: a service's own shapes of value, masked as their label +wherever they are logged (ECSCTX_MASK_VALUE_RULES). + +ecsctx names no such shape itself. A service lists its rules, and the engine +asks them about every mapping the key walk meets, every string that is JSON +text, and every JSON object written in free text or in a body redact_body +masks: a matching value is the rule's label, never hashed, in every pack. +These tests use a rule of their own. Every test runs with a keyset and +without one. +""" + +import json +import logging + +import pytest +from django.test import override_settings + +from ecsctx.contrib.django.checks import find_masking_errors +from ecsctx.contrib.net import redact_body +from ecsctx.masking import ( + ValueRule, + configure_masking_value_rules, + get_masking_value_rules, +) +from ecsctx.masking.config import configure_masking_packs +from ecsctx.masking.filters import MaskPIIFilter +from ecsctx.masking.patterns import ALL_PACKS, mask_by_patterns, rules_for +from ecsctx.pii import configure_pii +from ecsctx.processors import mask_sensitive_data + +LABEL = "[SAD-MASKED]" +SECRET = "c2VjcmV0LWJsb2I=" +BLOB = {"kind": "vault-blob", "payload": SECRET, "meta": {"size": 3, "tags": ["a"]}} +NOT_A_BLOB = {"kind": "note", "payload": "hello"} +ASKED: list = [] + + +def _is_blob(value) -> bool: + ASKED.append(value) + return value.get("kind") == "vault-blob" and "payload" in value + + +BLOB_RULE = ValueRule("sad", _is_blob, hints=("vault-blob",)) +RULES = (BLOB_RULE,) + + +@pytest.fixture(autouse=True, params=["keyset", "no-keyset"]) +def mode(request, token_keyset_path): + if request.param == "keyset": + configure_pii(token_keyset_path=token_keyset_path, env="test") + ASKED.clear() + return request.param + + +@pytest.fixture +def blob_rules(): + configure_masking_value_rules(RULES) + + +def _walk(event: dict) -> dict: + return mask_sensitive_data(None, "info", json.loads(json.dumps(event))) + + +def _text(text: str, packs=ALL_PACKS) -> str: + return mask_by_patterns(text, rules_for(frozenset(packs))) + + +def _message(msg, *args, packs=None) -> str: + record = logging.LogRecord("t", logging.INFO, __file__, 0, msg, args, None) + MaskPIIFilter(packs=packs).filter(record) + return record.getMessage() + + +@pytest.mark.usefixtures("blob_rules") +class TestAConfiguredRule: + @pytest.mark.parametrize("key", ["payload", "token", "password", "card", "customer", "udf9"]) + def test_a_mapping_under_any_key_is_its_label(self, key): + assert _walk({key: BLOB}) == {key: LABEL} + + @pytest.mark.parametrize("key", ["payload", "token", "note"]) + def test_json_text_under_any_key_is_its_label(self, key): + assert _walk({key: json.dumps(BLOB)}) == {key: LABEL} + + def test_json_text_holding_one_deeper_under_a_credential_key_is_the_label(self): + # Hashed whole, the token was a keyed hash of the value. + assert _walk({"token": json.dumps({"wrapper": BLOB})}) == {"token": LABEL} + + def test_in_a_container_and_a_list_the_rest_reads_as_before(self): + masked = _walk({"body": {"blob": BLOB, "amount": "10.000"}, "items": [BLOB, "x"]}) + assert masked == {"body": {"blob": LABEL, "amount": "10.000"}, "items": [LABEL, "x"]} + + @pytest.mark.parametrize("packs", [("default",), tuple(sorted(ALL_PACKS))], ids=["default", "pci"]) + def test_an_object_in_a_message_is_its_label(self, packs): + assert _message(f"got {json.dumps(BLOB)} back", packs=packs) == f"got {LABEL} back" + assert _text(f"got {BLOB!r} back", packs) == f"got {LABEL} back" + + def test_in_json_text_the_label_is_a_json_string(self): + text = "sent " + json.dumps({"blob": BLOB, "amount": "10.000"}) + masked = _text(text) + assert masked == f'sent {{"blob": "{LABEL}", "amount": "10.000"}}' + assert json.loads(masked[5:]) + + def test_json_in_a_json_string_is_its_escaped_label(self): + text = "sent " + json.dumps({"blob": json.dumps(BLOB), "amount": "10.000"}) + assert _text(text) == f'sent {{"blob": "{LABEL}", "amount": "10.000"}}' + + def test_redact_body_masks_it_in_json_and_in_an_element(self): + assert json.loads(redact_body(json.dumps({"blob": BLOB, "amount": "1"}))) == {"blob": LABEL, "amount": "1"} + assert redact_body(f"{json.dumps(BLOB)}1") == f"{LABEL}1" + encoded = json.dumps(BLOB).replace('"', """) + assert redact_body(f"{encoded}") == f"{LABEL}" + + def test_masking_twice_masks_once_and_hashes_nothing(self): + for masked in (_text(f"a {json.dumps(BLOB)} b"), redact_body(json.dumps({"t": BLOB})), _walk({"token": BLOB})): + text = json.dumps(masked) if isinstance(masked, dict) else masked + assert SECRET not in text + assert "ptok:v1:" not in text + once = _text(f"a {json.dumps(BLOB)} b") + assert _text(once) == once + assert redact_body(once) == once + + def test_anything_else_reads_as_before(self): + assert _walk({"payload": NOT_A_BLOB}) == {"payload": NOT_A_BLOB} + text = f"got {json.dumps(NOT_A_BLOB)} back, vault-blob mentioned" + assert _text(text) == text + + def test_text_holding_none_of_its_hints_is_never_parsed_to_ask(self): + ASKED.clear() + text = f"got {json.dumps(NOT_A_BLOB)} back" + assert _text(text) == text + assert redact_body(text) == text + assert ASKED == [] + + def test_the_label_is_the_rules_field_type(self): + configure_masking_value_rules([ValueRule("secret", lambda value: "vault" in value)]) + assert _walk({"x": {"vault": 1}}) == {"x": "[SECRET-MASKED]"} + + +class TestConfiguration: + def test_an_explicit_call_wins_and_none_goes_back(self): + configure_masking_value_rules(RULES) + assert get_masking_value_rules() == RULES + configure_masking_value_rules(None) + assert BLOB_RULE not in get_masking_value_rules() + + def test_a_dotted_path_to_a_rule_or_a_collection(self): + configure_masking_value_rules(["tests.test_masking_value_rules.BLOB_RULE"]) + assert get_masking_value_rules() == (BLOB_RULE,) + configure_masking_value_rules("tests.test_masking_value_rules.RULES") + assert get_masking_value_rules() == RULES + + def test_the_django_setting(self): + with override_settings(ECSCTX_MASK_VALUE_RULES=["tests.test_masking_value_rules.RULES"]): + assert _walk({"x": BLOB}) == {"x": LABEL} + + def test_the_environment_variable_names_import_paths(self, monkeypatch): + monkeypatch.setenv("ECSCTX_MASK_VALUE_RULES", "tests.test_masking_value_rules.RULES") + assert _walk({"x": BLOB}) == {"x": LABEL} + + @pytest.mark.parametrize("item", ["tests.test_masking_value_rules.NOPE", "no_such_module.RULES", "RULES", 42]) + def test_an_explicit_item_that_is_not_a_rule_raises(self, item): + with pytest.raises(ValueError, match="value rule|import"): + configure_masking_value_rules([item]) + + def test_a_setting_item_that_is_not_one_is_dropped_warned_and_reported(self, monkeypatch): + monkeypatch.setenv("ECSCTX_MASK_VALUE_RULES", "no_such_module.RULES,tests.test_masking_value_rules.RULES") + with pytest.warns(RuntimeWarning, match="no_such_module"): + assert _walk({"x": BLOB}) == {"x": LABEL} + [error] = [error for error in find_masking_errors({}) if "ECSCTX_MASK_VALUE_RULES" in error] + assert "no_such_module.RULES" in error + + def test_a_rule_that_raises_leaves_the_marker_never_an_exception(self): + def broken(_value): + raise RuntimeError("rule failed") + + configure_masking_value_rules([ValueRule("sad", broken)]) + record = logging.LogRecord("t", logging.INFO, __file__, 0, "got %s", ([{"a": 1}],), None) + assert MaskPIIFilter().filter(record) is True + assert record.getMessage() == "[MASKING-FAILED: RuntimeError]" + + +def test_every_pack_asks_it(blob_rules): + for packs in (["default"], sorted(ALL_PACKS)): + configure_masking_packs(packs) + assert _walk({"event": f"got {json.dumps(BLOB)}", "blob": BLOB}) == {"event": f"got {LABEL}", "blob": LABEL} diff --git a/tests/test_masking_xml_bodies.py b/tests/test_masking_xml_bodies.py new file mode 100644 index 0000000..78044fc --- /dev/null +++ b/tests/test_masking_xml_bodies.py @@ -0,0 +1,534 @@ +"""An XML body masks as a JSON or form body does (#160054). + +KNET's KPay gateway takes XML: Connect's outbound caller logs every request +body through `redact_body` into `http.request.body.content`, and on 0.15.5 +both `redact_body` and `mask_sensitive_data` left `…` in +clear -- neither the body rules nor the credential text rules read an element. + +`redact_body` classifies an element's local name as it classifies a query, +form or JSON key: a credential name masks its text as `mask_secret` masks a +credential, and a card, CVV or other SAD name by that type. The text rules +gain both element forms, for a string that never went through +`redact_body`. Every test runs with a keyset and without one. +""" + +import json +import logging +import time + +import pytest + +from ecsctx.contrib.net import configure_redaction, redact_body, redact_url +from ecsctx.masking.filters import MaskPIIFilter +from ecsctx.masking.patterns import ALL_PACKS, mask_by_patterns, mask_secret, rules_for +from ecsctx.pii import configure_pii, is_configured, tokenize +from ecsctx.pii.crypto import hmac_tokenize +from ecsctx.processors import mask_sensitive_data + +LABEL = "[SECRET-MASKED]" +PASSWORD = "S3cretPassw0rd" + +# KPay's bodies, with fake values. 1 pays with an Apple Pay token in udf9, 8 +# is the inquiry action, 2 the refund action. +APPLE_PAY_TOKEN = ( + '{"paymentData":{"data":"ZGF0YWJsb2JkYXRhYmxvYg==","signature":"c2ln","header":{"publicKeyHash":"cGtoYXNo",' + '"ephemeralPublicKey":"ZXBr","transactionId":"abc123"},"version":"EC_v1"}}' +) +KPAY_PAY = ( + "TRANPORTAL123{password}110.000" + "TRK1{udf9}" +) +KPAY_INQUIRY = ( + "TRANPORTAL123{password}810.000" + "202612345TRK1TrackID" +) +KPAY_REFUND = ( + "TRANPORTAL123{password}25.000" + "202612345TRK1TrackID" +) +KPAY_BODIES = { + "pay": KPAY_PAY.replace("{udf9}", APPLE_PAY_TOKEN), + "inquiry": KPAY_INQUIRY, + "refund": KPAY_REFUND, +} +READABLE = ["TRANPORTAL123", "10.000", "TRK1", "202612345"] + + +@pytest.fixture(autouse=True, params=["keyset", "no-keyset"]) +def mode(request, token_keyset_path): + if request.param == "keyset": + configure_pii(token_keyset_path=token_keyset_path, env="test") + return request.param + + +def secret(value: str) -> str: + """A credential as it must read in the mode in force.""" + return tokenize(value, "secret") if is_configured() else LABEL + + +def _processor(content: str) -> str: + event = mask_sensitive_data(None, "info", {"http": {"request": {"body": {"content": content}}}}) + return event["http"]["request"]["body"]["content"] + + +def _text(content: str) -> str: + return mask_by_patterns(content, rules_for(ALL_PACKS)) + + +def _default_text(content: str) -> str: + return mask_by_patterns(content, rules_for(frozenset({"default"}))) + + +def _message(text: str, packs: tuple[str, ...]) -> str: + """``text`` as a record's message, masked by a filter with ``packs``.""" + record = logging.LogRecord("t", logging.INFO, __file__, 0, text, None, None) + MaskPIIFilter(packs=packs).filter(record) + return record.getMessage() + + +PACKS = [pytest.param(("default",), id="default"), pytest.param(("default", "pci"), id="pci")] + + +def _body(name: str, password: str) -> str: + return KPAY_BODIES[name].replace("{password}", password) + + +def _outside_udf9(body: str) -> str: + """The body without udf9's text: the wallet token is Ottu's value rule's + (test_contrib_ottu_wallets.py).""" + head, _, rest = body.partition("") + return head + rest.partition("")[2] + + +class TestKPayBodies: + @pytest.mark.parametrize("name", sorted(KPAY_BODIES)) + def test_redact_body_masks_the_password_and_keeps_the_rest(self, name): + body = _body(name, PASSWORD) + expected = _body(name, secret(PASSWORD)) + assert _outside_udf9(redact_body(body)) == _outside_udf9(expected) + + @pytest.mark.parametrize("name", sorted(KPAY_BODIES)) + def test_the_processor_masks_a_raw_xml_body(self, name): + body = _body(name, PASSWORD) + expected = _body(name, secret(PASSWORD)) + masked = _processor(body) + assert PASSWORD not in masked + assert _outside_udf9(masked) == _outside_udf9(expected) + for element in READABLE: + assert element in masked or element not in body + + @pytest.mark.parametrize("name", sorted(KPAY_BODIES)) + def test_each_path_gives_the_token_the_key_walk_gives(self, name): + walked = mask_sensitive_data(None, "info", {"password": PASSWORD})["password"] + body = _body(name, PASSWORD) + for masked in (redact_body(body), _processor(body), _text(body), MaskPIIFilter()._mask_value(body)): + assert f"{walked}" in masked + + @pytest.mark.parametrize("name", sorted(KPAY_BODIES)) + def test_masking_twice_masks_once(self, name): + body = _body(name, PASSWORD) + once = redact_body(body) + assert redact_body(once) == once + assert _default_text(once) == once + assert _processor(once) == once + texts = _text(body) + assert _text(texts) == texts + assert redact_body(texts) == texts + assert _outside_udf9(_text(once)) == _outside_udf9(texts) + assert "ptok:v1:ptok:v1:" not in once + + +class TestRedactBodyReadsAnElementsName: + @pytest.mark.parametrize( + "element", + [ + "{}", + "{}", + "{}", + '{}', + '{}', + "{}", + "{}", + "{}", + "{}", + "{}", + ], + ) + def test_a_credential_names_text_is_masked_as_a_credential(self, element): + assert redact_body(element.format(PASSWORD)) == element.format(secret(PASSWORD)) + + @pytest.mark.parametrize("name", ["author", "design", "monkey", "userType", "keyword"]) + def test_a_name_holding_a_credential_hint_is_a_credential_as_a_query_param_is(self, name): + # Matched by substring, as redact_url matches a param's name (`auth`, + # `sign`, `key`, `user`): one reading across a URL and a body, and a + # cost of failing closed that is accepted. + assert redact_body(f"<{name}>Jane") == f"<{name}>{secret('Jane')}" + assert redact_url(f"https://x.example/?{name}=Jane") == f"https://x.example/?{name}={secret('Jane')}" + + def test_a_key_the_service_listed_is_a_credential_too(self): + configure_redaction(extra_secret_keys=["merchant_code"]) + assert redact_body(f"{PASSWORD}") == ( + f"{secret(PASSWORD)}" + ) + + @pytest.mark.parametrize( + ("element", "masked"), + [ + ("4111111111111111", "411111******1111"), + ("4508750000001019", "450875******1019"), + ("123", "[CVV-MASKED]"), + ("1234", "[CVV-MASKED]"), + ("1234", "[SAD-MASKED]"), + ("123", "[CVV-MASKED]"), + ], + ) + def test_a_card_cvv_or_sad_names_text_is_masked_by_that_type(self, element, masked): + assert redact_body(element) == masked + + @pytest.mark.parametrize( + "element", + [ + "2712", + "TRANPORTAL123", + "8", + "10.000", + "TRK1", + "202612345", + "TrackID", + "414", + ], + ) + def test_an_element_no_key_rule_names_is_left_as_it_is(self, element): + assert redact_body(element) == element + + @pytest.mark.parametrize( + ("element", "masked"), + [ + ('{}', '{}'), + ("{}", "{}"), + # A quote nothing closes before the next tag: the first `>` ends + # the start tag, as before, and the rest is the credential's. + ('{}'), + ], + ) + def test_a_close_angle_in_a_quoted_attribute_is_the_attributes(self, element, masked): + body = element.format(PASSWORD) + assert redact_body(body) == masked.format(secret(PASSWORD)) + assert _default_text(body) == masked.format(secret(PASSWORD)) + + @pytest.mark.parametrize( + ("element", "masked"), + [ + ('123', '[CVV-MASKED]'), + ('4111111111111111', '411111******1111'), + ( + '4111111111111111', + '411111******1111', + ), + ], + ) + def test_a_card_elements_quoted_attribute_is_kept_whole_too(self, element, masked): + assert redact_body(element) == masked + assert _default_text(element) == masked + + def test_a_credential_holding_a_card_number_run_is_the_label(self): + # As a JSON or form value is, in every pack. + assert redact_body("abc4111111111111111xyz") == f"{LABEL}" + + def test_an_entity_in_a_credential_is_masked_as_it_decodes(self): + assert redact_body("a&b<c") == f"{mask_secret('a&b" + + def test_a_cdata_credential_is_masked_as_its_content(self): + assert redact_body(f"") == f"{secret(PASSWORD)}" + + def test_a_credential_with_children_is_masked_whole(self): + masked = redact_body("abc123x1") + assert masked == f"{mask_secret('abc123x')}1" + + @pytest.mark.parametrize( + "body", + [ + # No end tag, no element: a route is a start tag's shape. + '{"route": "/v1/cards//"}', + "cut short", + ], + ) + def test_a_start_tag_no_end_tag_closes_is_left_as_it_is(self, body): + assert redact_body(body) == body + + @pytest.mark.parametrize("body", ["", "", "", "1"]) + def test_an_empty_credential_is_left_as_it_is(self, body): + assert redact_body(body) == body + + def test_masked_twice_it_is_masked_once(self): + for element in ("{}", "{}", "{}"): + once = redact_body(element.format("4111111111111111")) + assert redact_body(once) == once + + +class TestTextInAnElement: + def test_json_in_an_element_goes_through_the_body_rules(self): + body = '{"password": "s3cr3t", "amount": "1.000"}1' + assert redact_body(body) == f'{{"password": "{secret("s3cr3t")}", "amount": "1.000"}}1' + + def test_encoded_json_in_an_element_goes_through_them_as_it_decodes(self): + body = "{"password": "s3cr3t"}" + assert redact_body(body) == f'{{"password": "{secret("s3cr3t")}"}}' + + def test_a_form_body_in_an_element_keeps_its_closing_tag(self): + body = "password=s3cr3t&cvv=1231" + masked = redact_body(body) + assert masked == f"password={secret('s3cr3t')}&cvv=[CVV-MASKED]1" + assert redact_body(masked) == masked + + def test_a_form_value_in_an_element_keeps_its_closing_tag_in_the_text_rules_too(self): + text = "password=s3cr3t1" + assert _text(text) == f"password={secret('s3cr3t')}1" + assert redact_body(text) == _text(text) + + +class TestTheCredentialTextRule: + """For a string that never went through `redact_body`: every pack.""" + + @pytest.mark.parametrize( + "element", + [ + "{}", + '{}', + "{}", + "{}", + "{}", + ], + ) + def test_an_element_is_masked_in_the_default_pack(self, element): + assert _default_text(f"sent {element.format(PASSWORD)} ok") == f"sent {element.format(secret(PASSWORD))} ok" + + def test_the_value_runs_to_the_closing_tag(self): + value = "correct horse & 'battery'" + assert _text(f"{value}") == f"{mask_secret(value)}" + + def test_an_authorization_element_is_the_scheme_and_the_credential(self): + walked = mask_sensitive_data(None, "info", {"Authorization": "Bearer abc123def456"})["Authorization"] + assert _text("Bearer abc123def456") == f"{walked}" + + def test_a_token_after_the_scheme_is_not_hashed_again(self): + # What masking the credential alone leaves, as rule 7 reads it. + text = f"Bearer {hmac_tokenize('abc', bytes(32), 'secret', 'test')}" + assert _text(text) == text + + @pytest.mark.parametrize( + "text", + [ + # A route in a message is a start tag's shape; no end tag closes it. + "api request received: DELETE /v1/cards//", + "api response sent: DELETE /v1/cards// (204)", + "enter here", + "", + "", + "blue", + "on", + ], + ) + def test_prose_and_other_names_are_left_as_they_are(self, text): + assert _text(text) == text + + def test_a_value_masking_produced_passes_through(self): + for value in (secret(PASSWORD), LABEL, "[CVV-MASKED]"): + text = f"{value}" + assert _text(text) == text + + def test_the_processor_masks_a_json_string_holding_xml(self): + masked = _processor(json.dumps({"xml": f"{PASSWORD}"})) + assert PASSWORD not in masked + assert secret(PASSWORD) in masked + + +class TestAnElementsEndTagIsFoundOnce: + """No end tag, no element -- and a text's end tags are found once: looked + for again from each start tag, a text holding many that nothing closes + took quadratic time, 160 KB of `` twelve seconds in the text + rule.""" + + @pytest.mark.parametrize( + "text", + [ + "" * 16000 + "", + "".join(f"" for i in range(8000)) + "", + "" * 16000 + "", + # redact_body's credential names, `key` among them, and a body is + # redacted before it is capped (loggable_body): every byte a + # gateway sends reaches it. + "" * 16000 + "", + "" * 16000 + "", + "".join(f"" for i in range(2000)) + "", + ], + ids=["one_name", "distinct_names", "cvv", "key", "token", "distinct_keys_and_tokens"], + ) + def test_many_start_tags_nothing_closes_take_linear_time(self, text): + for mask in (_default_text, _text, redact_body): + started = time.perf_counter() + assert mask(text) == text + assert time.perf_counter() - started < 0.25, mask.__name__ + + +# An element named as the key walk names a card, a CVV or other SAD, and its +# text as the key walk masks that key's value. +CARD_ELEMENTS = [ + ("123", "[CVV-MASKED]"), + ("1234", "[SAD-MASKED]"), + # A test card that fails Luhn, which the `pci` card rule leaves in text, + # is a card number under its name all the same. + ("5123450000000007", "512345******0007"), + ("5123450000000008", "512345******0008"), + ("4111111111111112", "411111******1112"), + ("4111 1111 1111 1111", "411111******1111"), + # What a card key refuses to show: a run longer than a card number. + ("4111111111111111111111", "[CARD-MASKED]"), + ( + '1234', + '[CVV-MASKED]', + ), + ("123", "[CVV-MASKED]"), + ("123", "[CVV-MASKED]"), + ("4111111111111111=27121010000012300", "[SAD-MASKED]"), + ( + "AgAAAAAABk4DWZ4C28yUQAAAAAA=", + "[SAD-MASKED]", + ), + ("AAABBEg0VhI0VniQEjRWAAAAAAA=", "[SAD-MASKED]"), + ("", "[CVV-MASKED]"), + ("4111111111111111", "411111******1111"), +] + +READABLE_ELEMENTS = [ + "100.000", + "10.000", + "2712", + "001", + "M", + "TRK1", + "414", +] + + +class TestTheCardCvvAndSadTextRule: + """For a string that never went through `redact_body` -- a message, an + exception -- in every pack, as the key walk reads a key: an element named + as a card, a CVV or other SAD masks its text by that type. A test card + that fails Luhn under `` is truncated, `pci` or not.""" + + @pytest.mark.parametrize("packs", PACKS) + @pytest.mark.parametrize(("element", "masked"), CARD_ELEMENTS) + def test_an_element_in_a_message_is_masked_by_its_name(self, element, masked, packs): + assert _message(f"sent {element} ok", packs) == f"sent {masked} ok" + + @pytest.mark.parametrize(("element", "masked"), CARD_ELEMENTS) + def test_every_path_masks_it_alike(self, element, masked): + assert _default_text(element) == masked + assert _text(element) == masked + assert _processor(element) == masked + assert redact_body(element) == masked + + @pytest.mark.parametrize("packs", PACKS) + @pytest.mark.parametrize("element", READABLE_ELEMENTS) + def test_an_element_no_card_key_names_stays_readable(self, element, packs): + text = f"sent {element} ok" + assert _message(text, packs) == text + assert redact_body(text) == text + + @pytest.mark.parametrize("packs", PACKS) + def test_masking_twice_masks_once(self, packs): + text = " ".join(element for element, _ in CARD_ELEMENTS) + once = _message(text, packs) + assert _message(once, packs) == once + assert redact_body(once) == once + + @pytest.mark.parametrize( + "element", + [ + "[CVV-MASKED]", + "[SAD-MASKED]", + "411111******1111", + "********0002", + "[CARD-MASKED]", + "", + "", + ], + ) + @pytest.mark.parametrize("packs", PACKS) + def test_masking_output_passes_through(self, element, packs): + assert _message(element, packs) == element + assert redact_body(element) == element + + @pytest.mark.parametrize("text", ["enter the here", "123", "GET /v1/cards//"]) + def test_a_start_tag_no_end_tag_closes_is_left_as_it_is(self, text): + assert _default_text(text) == text + + +class TestACardElementHoldingElements: + """A card object written as XML (CyberSource's ``) is read field by + field, as the key walk reads a card object's keys: a CVV or SAD field is + its label, a credential a credential, and any other field's text a card + value -- a card number under `` truncated in every pack, + an expiry or a type read through.""" + + CARD = ( + "411111111111111112" + "2027123001" + ) + MASKED = ( + "411111******111112" + "2027[CVV-MASKED]001" + ) + + @pytest.mark.parametrize("packs", PACKS) + def test_each_field_is_masked_by_its_name(self, packs): + assert _message(self.CARD, packs) == self.MASKED + assert redact_body(self.CARD) == self.MASKED + assert _default_text(self.CARD) == self.MASKED + + def test_a_cvv_beside_the_card_number_is_its_label(self): + text = "4111111111111111123" + masked = "411111******1111[CVV-MASKED]" + assert redact_body(text) == masked + assert _default_text(text) == masked + + def test_a_card_number_that_fails_luhn_is_truncated(self): + card = "\n 4111111111111112\n" + masked = "\n 411111******1112\n" + assert _default_text(card) == masked + assert redact_body(card) == masked + + def test_a_cvv_in_an_element_no_key_rule_names_is_masked(self): + text = "123VISA" + masked = "[CVV-MASKED]VISA" + assert _default_text(text) == masked + assert redact_body(text) == masked + + @pytest.mark.parametrize( + ("text", "masked"), + [ + ("123", "[CVV-MASKED]"), + ("0041234FFFFFFFFFF", "[SAD-MASKED]"), + ], + ) + def test_a_cvv_or_sad_element_holding_elements_is_its_label_whole(self, text, masked): + assert _default_text(text) == masked + assert redact_body(text) == masked + + def test_a_credential_in_a_card_object_is_still_a_credential(self): + text = "tok_live_abc123" + masked = f"{secret('tok_live_abc123')}" + assert _default_text(text) == masked + assert redact_body(text) == masked + + @pytest.mark.parametrize("name", ["password", "token"]) + def test_a_cvv_in_a_credential_is_its_label_before_the_credential_is_masked(self, name): + # As the key walk gives a CVV under a credential key its label: masked + # with the credential, it was part of the text the credential's token + # was a keyed hash of. The credential's token is taken over the + # masked text. + text = f"<{name}>123" + masked = f"<{name}>{mask_secret('[CVV-MASKED]')}" + assert _default_text(text) == masked + assert redact_body(text) == masked