Skip to content
This repository was archived by the owner on Apr 20, 2024. It is now read-only.

Commit 663a84f

Browse files
authored
Merge pull request #62 from nodes-vapor/feature/filter-headers
Add tests for the keyFilter feature & apply filter everywhere
2 parents ad2330d + d2b5c47 commit 663a84f

File tree

5 files changed

+210
-7
lines changed

5 files changed

+210
-7
lines changed

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,20 @@ enum BreadcrumbType {
115115
}
116116
```
117117

118+
#### Filter out fields from reports
119+
Usually you will receive information such as headers, query params or post body fields in the reports from Bugsnag. To ensure that you do not track sensitive information, you can configure Bugsnag with a list of fields that should be filtered out:
120+
121+
```swift
122+
BugsnagConfig(
123+
apiKey: "apiKey",
124+
releaseStage: "test",
125+
keyFilters: ["password", "email", "authorization", "lastname"]
126+
)
127+
```
128+
In this case Bugsnag Reports won't contain header fields, query params or post body json fields with the keys/names **password**, **email**, **authorization**, **lastname**.
129+
130+
⚠️ Note: in JSON bodies, this only works for the first level of fields and not for nested children.
131+
118132
## 🏆 Credits
119133

120134
This package is developed and maintained by the Vapor team at [Nodes](https://www.nodesagency.com).

Sources/Bugsnag/Bugsnag.swift

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ struct BugsnagEvent: Encodable {
3333
breadcrumbs: [BugsnagBreadcrumb],
3434
error: Error,
3535
httpRequest: HTTPRequest? = nil,
36-
keyFilters: [String],
36+
keyFilters: Set<String>,
3737
metadata: [String: CustomDebugStringConvertible],
3838
payloadVersion: String,
3939
severity: Severity,
@@ -109,16 +109,17 @@ struct BugsnagRequest: Encodable {
109109
let referer: String
110110
let url: String
111111

112-
init(httpRequest: HTTPRequest, keyFilters: [String]) {
112+
init(httpRequest: HTTPRequest, keyFilters: Set<String>) {
113113
self.body = BugsnagRequest.filter(httpRequest.body, using: keyFilters)
114114
self.clientIp = httpRequest.remotePeer.hostname
115-
self.headers = Dictionary(httpRequest.headers.map { $0 }) { first, second in second }
115+
let filteredHeaders = BugsnagRequest.filter(httpRequest.headers, using: keyFilters)
116+
self.headers = Dictionary(filteredHeaders.map { $0 }) { first, second in second }
116117
self.httpMethod = httpRequest.method.string
117118
self.referer = httpRequest.remotePeer.description
118-
self.url = httpRequest.urlString
119+
self.url = BugsnagRequest.filter(httpRequest.urlString, using: keyFilters)
119120
}
120121

121-
static private func filter(_ body: HTTPBody, using filters: [String]) -> String? {
122+
static private func filter(_ body: HTTPBody, using filters: Set<String>) -> String? {
122123
guard
123124
let data = body.data,
124125
let unwrap = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
@@ -131,6 +132,23 @@ struct BugsnagRequest: Encodable {
131132
let json = try? JSONSerialization.data(withJSONObject: filtered, options: [.prettyPrinted])
132133
return json.flatMap { String(data: $0, encoding: .utf8) }
133134
}
135+
136+
static private func filter(_ headers: HTTPHeaders, using filters: Set<String>) -> HTTPHeaders {
137+
var mutableHeaders = headers
138+
filters.forEach { mutableHeaders.remove(name: $0) }
139+
return mutableHeaders
140+
}
141+
142+
/**
143+
@discussion Currently returns the original (unfiltered) url if anything goes wrong.
144+
*/
145+
static private func filter(_ urlString: String, using filters: Set<String>) -> String {
146+
guard var urlComponents = URLComponents(string: urlString) else {
147+
return urlString
148+
}
149+
urlComponents.queryItems?.removeAll(where: { filters.contains($0.name) })
150+
return urlComponents.string ?? urlString
151+
}
134152
}
135153

136154
struct BugsnagThread: Encodable {

Sources/Bugsnag/BugsnagConfig.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ public struct BugsnagConfig {
33
let releaseStage: String
44
/// A version identifier, (eg. a git hash)
55
let version: String?
6-
let keyFilters: [String]
6+
let keyFilters: Set<String>
77
let shouldReport: Bool
88
let debug: Bool
99

@@ -18,7 +18,7 @@ public struct BugsnagConfig {
1818
self.apiKey = apiKey
1919
self.releaseStage = releaseStage
2020
self.version = version
21-
self.keyFilters = keyFilters
21+
self.keyFilters = Set(keyFilters)
2222
self.shouldReport = shouldReport
2323
self.debug = debug
2424
}

Tests/BugsnagTests/BugsnagTests.swift

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,4 +55,172 @@ final class BugsnagTests: XCTestCase {
5555
let request = Request(using: application)
5656
try reporter.report(NotFound(), on: request).wait()
5757
}
58+
59+
func testKeyFiltersWorkInRequestBody() throws {
60+
var capturedSendReportParameters: (
61+
host: String,
62+
headers: HTTPHeaders,
63+
body: Data,
64+
container: Container
65+
)?
66+
67+
let reporter = BugsnagReporter(
68+
config: .init(apiKey: "apiKey", releaseStage: "test", keyFilters: ["password", "email"]),
69+
sendReport: { host, headers, data, container in
70+
capturedSendReportParameters = (host, headers, data, container)
71+
return container.future(Response(http: HTTPResponse(status: .ok), using: container))
72+
})
73+
let application = try Application.test()
74+
let request = Request(using: application)
75+
request.http.method = .POST
76+
request.http.body = TestBody.default.httpBody
77+
78+
_ = try! reporter.report(NotFound(), on: request).wait()
79+
80+
guard let params = capturedSendReportParameters else {
81+
XCTFail()
82+
return
83+
}
84+
85+
let responseBody = try JSONDecoder().decode(BugsnagResponseBody<TestBody>.self, from: params.body)
86+
87+
guard let body = responseBody.events.first?.request?.body else {
88+
XCTFail("Unable to parse request body")
89+
return
90+
}
91+
XCTAssertNil(body.password, "test that password is removed")
92+
XCTAssertNil(body.email, "test that email is removed")
93+
XCTAssertEqual(body.hash, TestBody.default.hash, "test that hash is not altered")
94+
}
95+
96+
func testKeyFiltersWorkInHeaderFields() throws {
97+
var capturedSendReportParameters: (
98+
host: String,
99+
headers: HTTPHeaders,
100+
body: Data,
101+
container: Container
102+
)?
103+
104+
let reporter = BugsnagReporter(
105+
config: .init(apiKey: "apiKey", releaseStage: "test", keyFilters: ["password", "email"]),
106+
sendReport: { host, headers, data, container in
107+
capturedSendReportParameters = (host, headers, data, container)
108+
return container.future(Response(http: HTTPResponse(status: .ok), using: container))
109+
})
110+
let application = try Application.test()
111+
let request = Request(using: application)
112+
request.http.method = .POST
113+
request.http.body = TestBody.default.httpBody
114+
var headers = request.http.headers
115+
headers.add(name: HTTPHeaderName("password"), value: TestBody.default.password!)
116+
headers.add(name: HTTPHeaderName("email"), value: TestBody.default.email!)
117+
headers.add(name: HTTPHeaderName("hash"), value: TestBody.default.hash!)
118+
request.http.headers = headers
119+
120+
_ = try! reporter.report(NotFound(), on: request).wait()
121+
122+
guard let params = capturedSendReportParameters else {
123+
XCTFail()
124+
return
125+
}
126+
127+
let responseBody = try JSONDecoder().decode(BugsnagResponseBody<TestBody>.self, from: params.body)
128+
129+
guard let responseHeaders = responseBody.events.first?.request?.headers else {
130+
XCTFail("Unable to parse response headers")
131+
return
132+
}
133+
134+
XCTAssertNil(responseHeaders["password"], "test that password is removed")
135+
XCTAssertNil(responseHeaders["email"], "test that email is removed")
136+
XCTAssertEqual(responseHeaders["hash"], TestBody.default.hash!, "test that hash is not altered")
137+
}
138+
139+
func testKeyFiltersWorkInURLQueryParams() throws {
140+
var capturedSendReportParameters: (
141+
host: String,
142+
headers: HTTPHeaders,
143+
body: Data,
144+
container: Container
145+
)?
146+
147+
let reporter = BugsnagReporter(
148+
config: .init(apiKey: "apiKey", releaseStage: "test", keyFilters: ["password", "email"]),
149+
sendReport: { host, headers, data, container in
150+
capturedSendReportParameters = (host, headers, data, container)
151+
return container.future(Response(http: HTTPResponse(status: .ok), using: container))
152+
})
153+
let application = try Application.test()
154+
let request = Request(using: application)
155+
request.http.url = URL(string: "http://foo.bar.com/?password=\(TestBody.default.password!)&email=\(TestBody.default.email!)&hash=\(TestBody.default.hash!)")!
156+
request.http.method = .POST
157+
request.http.body = TestBody.default.httpBody
158+
var headers = request.http.headers
159+
headers.add(name: HTTPHeaderName("password"), value: TestBody.default.password!)
160+
headers.add(name: HTTPHeaderName("email"), value: TestBody.default.email!)
161+
headers.add(name: HTTPHeaderName("hash"), value: TestBody.default.hash!)
162+
request.http.headers = headers
163+
164+
_ = try! reporter.report(NotFound(), on: request).wait()
165+
166+
guard let params = capturedSendReportParameters else {
167+
XCTFail()
168+
return
169+
}
170+
171+
let responseBody = try JSONDecoder().decode(BugsnagResponseBody<TestBody>.self, from: params.body)
172+
173+
guard let responseURLString = responseBody.events.first?.request?.url else {
174+
XCTFail("Unable to parse response url")
175+
return
176+
}
177+
178+
let urlComponents = URLComponents(string: responseURLString)
179+
let passwordItem = urlComponents?.queryItems?.filter { $0.name == "password" }.last
180+
let emailItem = urlComponents?.queryItems?.filter { $0.name == "email" }.last
181+
let hashItem = urlComponents?.queryItems?.filter { $0.name == "hash" }.last
182+
183+
XCTAssertNil(passwordItem, "test that password is removed")
184+
XCTAssertNil(emailItem, "test that email is removed")
185+
XCTAssertEqual(hashItem?.value, TestBody.default.hash!, "test that hash is not altered")
186+
}
187+
}
188+
189+
struct TestBody: Codable {
190+
var password: String?
191+
var email: String?
192+
var hash: String?
193+
194+
static var `default`: TestBody {
195+
return .init(password: "TopSecret", email: "[email protected]", hash: "myAwesomeHash")
196+
}
197+
198+
var httpBody: HTTPBody {
199+
return try! HTTPBody(data: JSONEncoder().encode(self))
200+
}
201+
}
202+
203+
struct BugsnagResponseBody<T: Codable>: Codable {
204+
struct Event: Codable {
205+
struct Request: Codable {
206+
let body: T?
207+
let headers: [String: String]?
208+
let url: String?
209+
210+
// custom decoding needed as the format is JSON string (not JSON object)
211+
init(from decoder: Decoder) throws {
212+
let container = try decoder.container(keyedBy: CodingKeys.self)
213+
let bodyString = try container.decode(String.self, forKey: .body)
214+
guard let data = bodyString.data(using: .utf8) else {
215+
throw Abort(.internalServerError)
216+
}
217+
body = try JSONDecoder().decode(T.self, from: data)
218+
headers = try container.decode(Dictionary.self, forKey: .headers)
219+
url = try container.decode(String.self, forKey: .url)
220+
}
221+
}
222+
let request: Request?
223+
}
224+
let apiKey: String
225+
let events: [Event]
58226
}

Tests/BugsnagTests/XCTestManifests.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ extension BugsnagTests {
66
// `swift test --generate-linuxmain`
77
// to regenerate.
88
static let __allTests__BugsnagTests = [
9+
("testKeyFiltersWorkInHeaderFields", testKeyFiltersWorkInHeaderFields),
10+
("testKeyFiltersWorkInRequestBody", testKeyFiltersWorkInRequestBody),
11+
("testKeyFiltersWorkInURLQueryParams", testKeyFiltersWorkInURLQueryParams),
912
("testReportingCanBeDisabled", testReportingCanBeDisabled),
1013
("testSendReport", testSendReport),
1114
]

0 commit comments

Comments
 (0)