Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions src/channel_relay/pii/rules_fallback.json
Original file line number Diff line number Diff line change
Expand Up @@ -1481,6 +1481,52 @@
}
]
},
{
"id": "sabre.res.auth_approval_code",
"channel": "sabre",
"operation": "^(GetReservationRS|UpdateReservationRS)$",
"path": "//s19:RemarkLine/s19:Text | //s19:RemarkText | //s19:HistoryAssociationElement | //s19:AssociationChild/s19:Content | //s19:AssociationParent/s19:Content",
"namespaces": {
"s19": "http://webservices.sabre.com/pnrbuilder/v1_19"
},
"pii_type": "payment",
"method": "replace",
"replacement": "REDACTED",
"extract_patterns": [
{
"pattern": "AUTH-APV/(\\d{4,8})"
},
{
"pattern": "(?:XX)?AUTH/([A-Z0-9]{5,8})(?=[/ ])"
},
{
"pattern": "\\bCC APVL/([A-Z0-9]{5,8})(?=/)"
}
]
},
{
"id": "sabre.res.auth_card_fragment",
"channel": "sabre",
"operation": "^(GetReservationRS|UpdateReservationRS)$",
"path": "//s19:RemarkLine/s19:Text | //s19:RemarkText | //s19:HistoryAssociationElement | //s19:AssociationChild/s19:Content | //s19:AssociationParent/s19:Content",
"namespaces": {
"s19": "http://webservices.sabre.com/pnrbuilder/v1_19"
},
"pii_type": "payment",
"method": "replace",
"replacement": "REDACTED",
"extract_patterns": [
{
"pattern": "AUTH-[A-Z]+/([A-Z]{2}\\d{4})"
},
{
"pattern": "AUTH/[A-Z0-9]{4,10} \\*[A-Z]/([A-Z]{2}\\d{4})"
},
{
"pattern": "\\bCC APVL/[^/]*/TKT/[A-Z]{2}/([A-Z]{2}\\d{4})"
}
]
},
{
"id": "sabre.res.employee_id_attribute",
"channel": "sabre",
Expand Down Expand Up @@ -2265,6 +2311,26 @@
"method": "replace",
"replacement": "REDACTED"
},
{
"id": "sabre.trip.auth_approval_code",
"channel": "sabre",
"operation": "^Trip_SearchRS$",
"path": "//stl19:RemarkLine/stl19:Text | //stl19:RemarkText | //stl19:FactHistory/stl19:FreeText | //stl19:FactHistory/stl19:FullText",
"namespaces": {
"stl19": "http://webservices.sabre.com/pnrbuilder/v1_19"
},
"pii_type": "payment",
"method": "replace",
"replacement": "REDACTED",
"extract_patterns": [
{
"pattern": "AUTH-APV/(\\d{4,8})"
},
{
"pattern": "(?:XX)?AUTH/([A-Z0-9]{5,8})(?=[/ ])"
}
]
},
{
"id": "sabre.trip.card_fragment_remark",
"channel": "sabre",
Expand Down
49 changes: 49 additions & 0 deletions tests/unit/test_pii_sabre.py
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,55 @@ def test_ssn_in_remark_masked_one_way(self, baked_ruleset: RuleSet, pii_keyring:
assert counts["ssn"] >= 1


class TestCardAuthorizationRemarks:
"""Card authorization lines carry two pieces of payment data beyond the PAN: the acquirer
approval code and a ``<brand><last four>`` fragment.

This repo already classifies both as payment data elsewhere — ``amadeus.transaction.payment``
redacts ``approvalCode`` and ``sabre.etkt.payment_approval`` masks ``@ApprovalID`` — but the
reservation and trip-search remark forms were uncovered. ``GetReservationRS`` was the worst
case: ``XXAUTH/<approval> *Z/<brand><last4>`` passed through completely untouched, leaking the
last four digits of the card on the very operation a customer reported.
"""

def test_get_reservation_authorization_redacted(self, baked_ruleset: RuleSet, pii_keyring: Keyring) -> None:
redacted, _ = _redact(_fixture("get_reservation_pq_history_response.xml"), baked_ruleset, pii_keyring)
# Approval code and the brand+last-four fragment are both gone, from BOTH renderings of
# this authorization: the "XXAUTH/" line and the "CC APVL/" line.
assert b"S00000" not in redacted
assert b"VI0000" not in redacted
# Operational markers and the amount survive, so the lines stay recognisable.
assert b"XXAUTH/" in redacted
assert b"CC APVL/" in redacted
assert b"/TKT/DL/" in redacted
assert b"USD388.90 - USED" in redacted

def test_trip_search_approval_code_redacted(self, baked_ruleset: RuleSet, pii_keyring: Keyring) -> None:
redacted, _ = _redact(_fixture("trip_search_past_date_pnr_response.xml"), baked_ruleset, pii_keyring)
# Approval code appears in both the AUTH-APV and XXAUTH renderings.
assert b"121395" not in redacted
# Brand+last-four fragments alongside it.
assert b"AX0005" not in redacted
assert b"CA0000" not in redacted

def test_trip_search_authorization_operational_text_preserved(
self, baked_ruleset: RuleSet, pii_keyring: Keyring
) -> None:
redacted, _ = _redact(_fixture("trip_search_past_date_pnr_response.xml"), baked_ruleset, pii_keyring)
# Markers, amounts, dates and the status lines that carry no card data stay verbatim. The
# card brand word on its own is not sensitive; the last four beside it is.
for kept in (
b"AUTH-APV/",
b"AUTH-AMEX/",
b"AUTH-MSTR/",
b"AUTH-AVS NOT SUPPORTED/",
b"AUTH-CSC NOT SUPPLIED/",
b"USD423.20",
b"20JUN",
):
assert kept in redacted, kept


class TestTripSearchPassportDocuments:
"""Trip_SearchRS embeds a whole ``stl19:GetReservationRS``, so its APIS block is the same shape
the ``sabre.res.docs_*`` rules cover. A thinner ``sabre.trip.docs_*`` copy left the passport
Expand Down
Loading