1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
require_relative "../test_helper"
require "sentry-ruby"
require "pro_tacts/sentry_scrubber"
class SentryScrubberTest < Minitest::Test
CARD = <<~VCARD.chomp
BEGIN:VCARD
VERSION:3.0
UID:AB12C345
FN:Real Person
TEL:+1-555-1234
END:VCARD
VCARD
# Stands in for a Sentry event: the scrubber only reaches for #request,
# and the request only needs a body and headers.
Event = Struct.new(:request)
Request = Struct.new(:data, :headers)
def scrub(data, headers = {})
event = Event.new(Request.new(data, headers))
ProTacts::SentryScrubber.call(event, nil)
event.request.data
end
def test_a_card_is_redacted
refute_includes scrub(CARD), "Real Person"
end
def test_a_card_is_replaced_rather_than_emptied
assert_includes scrub(CARD), "vcard redacted"
end
# The redaction has to stop at END:VCARD, not run to the end of the body,
# or a card anywhere in a request would take the rest of it along.
def test_a_card_embedded_in_xml_is_redacted_without_eating_what_follows
scrubbed = scrub("<address-data>#{CARD}</address-data><href>/keep/me</href>")
refute_includes scrubbed, "Real Person"
assert_includes scrubbed, "<address-data>"
assert_includes scrubbed, "</address-data>"
assert_includes scrubbed, "<href>/keep/me</href>"
end
# (?~) is greedy to the longest run holding no complete END:VCARD, which
# runs into the marker and stops at END:VCAR. Without something forcing it
# back onto the boundary a stray "D" survives, so assert the exact result
# rather than just the absence of card content.
def test_no_fragment_of_the_end_marker_survives
assert_equal "<d>#{ProTacts::SentryScrubber::REDACTED}</d>TAIL", scrub("<d>#{CARD}</d>TAIL")
end
def test_content_between_two_cards_survives
scrubbed = scrub("#{CARD}\nMIDDLE\n#{CARD.sub('Real Person', 'Other Person')}\nTRAILING")
refute_includes scrubbed, "Real Person"
refute_includes scrubbed, "Other Person"
assert_includes scrubbed, "MIDDLE"
assert_includes scrubbed, "TRAILING"
end
# Only the unterminated card runs to the end; the complete one before it
# still stops at its own END:VCARD.
def test_a_complete_card_before_a_truncated_one_stops_at_its_end
scrubbed = scrub("#{CARD}\nMIDDLE\nBEGIN:VCARD\nFN:Trunc")
refute_includes scrubbed, "Real Person"
refute_includes scrubbed, "Trunc"
assert_includes scrubbed, "MIDDLE"
end
def test_several_cards_are_all_redacted
scrubbed = scrub("#{CARD}\n#{CARD.sub('Real Person', 'Other Person')}")
refute_includes scrubbed, "Real Person"
refute_includes scrubbed, "Other Person"
end
# Sentry truncates at 16KB, so a card can arrive without its END line.
def test_a_truncated_card_is_redacted
refute_includes scrub("BEGIN:VCARD\nVERSION:3.0\nFN:Real Pers"), "Real Pers"
end
def test_lowercase_markers_are_redacted
refute_includes scrub(CARD.downcase), "real person"
end
# The href in a multiget names a contact by UID, which is not a secret and
# is the useful part of a 404 report.
def test_hrefs_are_left_alone
body = "<href>/dav/addressbook/AB12C345-6789.vcf</href>"
assert_equal body, scrub(body)
end
def test_a_vcard_content_type_redacts_the_whole_body
scrubbed = scrub("garbled but still a card", "Content-Type" => "text/vcard; charset=utf-8")
assert_equal ProTacts::SentryScrubber::REDACTED, scrubbed
end
def test_form_bodies_are_walked
scrubbed = scrub({ "card" => CARD, "id" => "AB12C345" })
refute_includes scrubbed.fetch("card"), "Real Person"
assert_equal "AB12C345", scrubbed.fetch("id")
end
def test_a_nil_body_is_left_alone
assert_nil scrub(nil)
end
def test_an_event_without_a_request_is_returned_unchanged
event = Event.new(nil)
assert_same event, ProTacts::SentryScrubber.call(event, nil)
end
# Guards the field names against a sentry-ruby upgrade: this is the real
# interface Sentry builds from a Rack env, not a stand-in.
def test_it_scrubs_a_real_sentry_request_interface
env = Rack::MockRequest.env_for("/dav/addressbook/", method: "PUT", input: CARD)
env["CONTENT_TYPE"] = "text/xml"
request = Sentry::RequestInterface.new(env: env, send_default_pii: true, rack_env_whitelist: [])
assert_includes request.data, "Real Person", "precondition: Sentry captured the body"
ProTacts::SentryScrubber.call(Event.new(request), nil)
refute_includes request.data, "Real Person"
end
end