From 23ff3769aca3646e64ecbe5af45e57a82f31a538 Mon Sep 17 00:00:00 2001 From: Geoffrey White <40627776+geoffw0@users.noreply.github.com> Date: Mon, 31 Oct 2022 18:24:13 +0000 Subject: [PATCH 1/5] Swift: Add Alamofire tests for swift/cleartext-transmission. --- .../Security/CWE-311/SensitiveExprs.expected | 10 + .../Security/CWE-311/testAlamofire.swift | 218 ++++++++++++++++++ 2 files changed, 228 insertions(+) create mode 100644 swift/ql/test/query-tests/Security/CWE-311/testAlamofire.swift diff --git a/swift/ql/test/query-tests/Security/CWE-311/SensitiveExprs.expected b/swift/ql/test/query-tests/Security/CWE-311/SensitiveExprs.expected index 9e359b62e27..3c61ec86e95 100644 --- a/swift/ql/test/query-tests/Security/CWE-311/SensitiveExprs.expected +++ b/swift/ql/test/query-tests/Security/CWE-311/SensitiveExprs.expected @@ -1,3 +1,13 @@ +| testAlamofire.swift:150:45:150:45 | password | label:password, type:credential | +| testAlamofire.swift:152:51:152:51 | password | label:password, type:credential | +| testAlamofire.swift:154:38:154:38 | email | label:email, type:private information | +| testAlamofire.swift:159:26:159:26 | email | label:email, type:private information | +| testAlamofire.swift:171:35:171:35 | email | label:email, type:private information | +| testAlamofire.swift:177:35:177:35 | email | label:email, type:private information | +| testAlamofire.swift:187:65:187:65 | password | label:password, type:credential | +| testAlamofire.swift:195:64:195:64 | password | label:password, type:credential | +| testAlamofire.swift:205:62:205:62 | password | label:password, type:credential | +| testAlamofire.swift:213:65:213:65 | password | label:password, type:credential | | testCoreData.swift:48:15:48:15 | password | label:password, type:credential | | testCoreData.swift:51:24:51:24 | password | label:password, type:credential | | testCoreData.swift:58:15:58:15 | password | label:password, type:credential | diff --git a/swift/ql/test/query-tests/Security/CWE-311/testAlamofire.swift b/swift/ql/test/query-tests/Security/CWE-311/testAlamofire.swift new file mode 100644 index 00000000000..9056eebdbf1 --- /dev/null +++ b/swift/ql/test/query-tests/Security/CWE-311/testAlamofire.swift @@ -0,0 +1,218 @@ + +// --- Foundation stubs --- + +class NSObject { +} + +struct URL { +} + +struct URLRequest { +} + +class URLResponse: NSObject { +} + +class HTTPURLResponse : URLResponse { +} + +// --- Alamofire stubs --- + +protocol URLConvertible { +} + +extension String: URLConvertible { +} + +struct HTTPMethod { + static let get = HTTPMethod(rawValue: "GET") + static let post = HTTPMethod(rawValue: "POST") + + init(rawValue: String) {} +} + +struct HTTPHeaders { + init(_ dictionary: [String: String]) {} + + mutating func add(name: String, value: String) {} + mutating func update(name: String, value: String) {} +} + +extension HTTPHeaders: ExpressibleByDictionaryLiteral { + public init(dictionaryLiteral elements: (String, String)...) {} +} + +typealias Parameters = [String: Any] + +protocol ParameterEncoding { +} + +struct URLEncoding: ParameterEncoding { + static var `default`: URLEncoding { URLEncoding() } +} + +protocol ParameterEncoder { +} + +class URLEncodedFormParameterEncoder: ParameterEncoder { + static var `default`: URLEncodedFormParameterEncoder { URLEncodedFormParameterEncoder() } +} + +protocol RequestInterceptor { +} + +class Request { +} + +class DataRequest: Request { +} + +final class DataStreamRequest: Request { +} + +class DownloadRequest: Request { + struct Options: OptionSet { + let rawValue: Int + + init(rawValue: Int) { + self.rawValue = rawValue + } + } + + typealias Destination = + (_ temporaryURL: URL, _ response: HTTPURLResponse) -> + (destinationURL: URL, options: Options) +} + +class Session { + static let `default` = Session() + + typealias RequestModifier = (inout URLRequest) throws -> Void + + func request( + _ convertible: URLConvertible, + method: HTTPMethod = .get, + parameters: Parameters? = nil, + encoding: ParameterEncoding = URLEncoding.default, + headers: HTTPHeaders? = nil, + interceptor: RequestInterceptor? = nil, + requestModifier: RequestModifier? = nil) -> DataRequest { + return DataRequest() + } + + func request( + _ convertible: URLConvertible, + method: HTTPMethod = .get, + parameters: Parameters? = nil, + encoder: ParameterEncoder = URLEncodedFormParameterEncoder.default, + headers: HTTPHeaders? = nil, + interceptor: RequestInterceptor? = nil, + requestModifier: RequestModifier? = nil) -> DataRequest { + return DataRequest() + } + + func streamRequest( + _ convertible: URLConvertible, + method: HTTPMethod = .get, + headers: HTTPHeaders? = nil, + automaticallyCancelOnStreamError: Bool = false, + interceptor: RequestInterceptor? = nil, + requestModifier: RequestModifier? = nil) -> DataStreamRequest { + return DataStreamRequest() + } + + func download( + _ convertible: URLConvertible, + method: HTTPMethod = .get, + parameters: Parameters? = nil, + encoding: ParameterEncoding = URLEncoding.default, + headers: HTTPHeaders? = nil, + interceptor: RequestInterceptor? = nil, + requestModifier: RequestModifier? = nil, + to destination: DownloadRequest.Destination? = nil) -> DownloadRequest { + return DownloadRequest() + } + + // (there are many more variants of `request`, `streamRequest` and `download`) +} + +let AF = Session.default + +// --- tests --- + +struct MyEncodable: Encodable { + let value: String +} + +func test1(username: String, password: String, email: String, harmless: String) { + // sensitive data in URL + + AF.request("http://example.com/login?p=" + password) // BAD [NOT DETECTED] + AF.request("http://example.com/login?h=" + harmless) // GOOD (not sensitive) + AF.streamRequest("http://example.com/login?p=" + password) // BAD [NOT DETECTED] + AF.streamRequest("http://example.com/login?h=" + harmless) // GOOD (not sensitive) + AF.download("http://example.com/" + email + ".html") // BAD [NOT DETECTED] + AF.download("http://example.com/" + harmless + ".html") // GOOD (not sensitive) + + // sensitive data in parameters + + let params1 = ["value": email] + let params2 = ["value": harmless] + + AF.request("http://example.com/", parameters: params1) // BAD [NOT DETECTED] + AF.request("http://example.com/", parameters: params2) // GOOD (not sensitive) + AF.request("http://example.com/", parameters: params1, encoding: URLEncoding.default) // BAD [NOT DETECTED] + AF.request("http://example.com/", parameters: params2, encoding: URLEncoding.default) // GOOD (not sensitive) + AF.request("http://example.com/", parameters: params1, encoder: URLEncodedFormParameterEncoder.default) // BAD [NOT DETECTED] + AF.request("http://example.com/", parameters: params2, encoder: URLEncodedFormParameterEncoder.default) // GOOD (not sensitive) + AF.download("http://example.com/", parameters: params1) // BAD [NOT DETECTED] + AF.download("http://example.com/", parameters: params2) // GOOD (not sensitive) + + let params3 = ["values": ["...", email, "..."]] + let params4 = ["values": ["...", harmless, "..."]] + + AF.request("http://example.com/", method:.post, parameters: params3) // BAD [NOT DETECTED] + AF.request("http://example.com/", method:.post, parameters: params4) // GOOD (not sensitive) + + let params5 = MyEncodable(value: email) + let params6 = MyEncodable(value: harmless) + + AF.request("http://example.com/", parameters: params5) // BAD [NOT DETECTED] + AF.request("http://example.com/", parameters: params6) // GOOD (not sensitive) + + // request headers + // - in real usage a password here would normally be base64 encoded for transmission + // - the risk is greatly reduced (but not eliminated) if HTTPS is used + + let headers1: HTTPHeaders = ["Authorization": username + ":" + password] + let headers2: HTTPHeaders = ["Value": harmless] + + AF.request("http://example.com/", headers: headers1) // BAD [NOT DETECTED] + AF.request("http://example.com/", headers: headers2) // GOOD (not sensitive) + AF.streamRequest("http://example.com/", headers: headers1) // BAD [NOT DETECTED] + AF.streamRequest("http://example.com/", headers: headers2) // GOOD (not sensitive) + + let headers3 = HTTPHeaders(["Authorization": username + ":" + password]) + let headers4 = HTTPHeaders(["Value": harmless]) + + AF.request("http://example.com/", headers: headers3) // BAD [NOT DETECTED] + AF.request("http://example.com/", headers: headers4) // GOOD (not sensitive) + AF.download("http://example.com/", headers: headers1) // BAD [NOT DETECTED] + AF.download("http://example.com/", headers: headers2) // GOOD (not sensitive) + + var headers5 = HTTPHeaders([:]) + var headers6 = HTTPHeaders([:]) + headers5.add(name: "Authorization", value: username + ":" + password) + headers6.add(name: "Data", value: harmless) + + AF.request("http://example.com/", headers: headers5) // BAD [NOT DETECTED] + AF.request("http://example.com/", headers: headers6) // GOOD (not sensitive) + + var headers7 = HTTPHeaders([:]) + var headers8 = HTTPHeaders([:]) + headers7.update(name: "Authorization", value: username + ":" + password) + headers8.update(name: "Data", value: harmless) + + AF.request("http://example.com/", headers: headers7) // BAD [NOT DETECTED] + AF.request("http://example.com/", headers: headers8) // GOOD (not sensitive) +} From d97682991df5a8ac0b5b72c654fe5e4ea8cea767 Mon Sep 17 00:00:00 2001 From: Geoffrey White <40627776+geoffw0@users.noreply.github.com> Date: Tue, 1 Nov 2022 15:35:50 +0000 Subject: [PATCH 2/5] Swift: Add Alamofire sink for cpp/cleartext-transmission. --- .../codeql/swift/elements/expr/ApplyExpr.qll | 8 ++++++++ .../Security/CWE-311/CleartextTransmission.ql | 19 +++++++++++++++++++ .../CWE-311/CleartextTransmission.expected | 12 ++++++++++++ .../Security/CWE-311/testAlamofire.swift | 6 +++--- 4 files changed, 42 insertions(+), 3 deletions(-) diff --git a/swift/ql/lib/codeql/swift/elements/expr/ApplyExpr.qll b/swift/ql/lib/codeql/swift/elements/expr/ApplyExpr.qll index 14bc6302c2b..cf11158df92 100644 --- a/swift/ql/lib/codeql/swift/elements/expr/ApplyExpr.qll +++ b/swift/ql/lib/codeql/swift/elements/expr/ApplyExpr.qll @@ -19,6 +19,14 @@ class ApplyExpr extends Generated::ApplyExpr { /** Gets the method qualifier, if this is applying a method */ Expr getQualifier() { none() } + /** + * Gets the argument of this `ApplyExpr` called `label` (if any). + */ + final Argument getArgumentWithLabel(string label) { + result = getAnArgument() and + result.getLabel() = label + } + override string toString() { result = "call to " + this.getStaticTarget().toString() or diff --git a/swift/ql/src/queries/Security/CWE-311/CleartextTransmission.ql b/swift/ql/src/queries/Security/CWE-311/CleartextTransmission.ql index 92977f5cfd5..21c1c538462 100644 --- a/swift/ql/src/queries/Security/CWE-311/CleartextTransmission.ql +++ b/swift/ql/src/queries/Security/CWE-311/CleartextTransmission.ql @@ -54,6 +54,25 @@ class Url extends Transmitted { } } +/** + * An `Expr` that transmitted through the Alamofire library. + */ +class AlamofireTransmitted extends Transmitted { + AlamofireTransmitted() { + // sinks are the first argument containing the URL, and the `parameters` + // and `headers` arguments to appropriate methods of `Session`. + exists(CallExpr call, string fName | + call.getStaticTarget().(MethodDecl).hasQualifiedName("Session", fName) and + fName.regexpMatch("(request|streamRequest|download)\\(.*") and + ( + call.getArgument(0).getExpr() = this or + call.getArgumentWithLabel("parameters").getExpr() = this or + call.getArgumentWithLabel("headers").getExpr() = this + ) + ) + } +} + /** * A taint configuration from sensitive information to expressions that are * transmitted over a network. diff --git a/swift/ql/test/query-tests/Security/CWE-311/CleartextTransmission.expected b/swift/ql/test/query-tests/Security/CWE-311/CleartextTransmission.expected index de02d0db461..d8328252a1c 100644 --- a/swift/ql/test/query-tests/Security/CWE-311/CleartextTransmission.expected +++ b/swift/ql/test/query-tests/Security/CWE-311/CleartextTransmission.expected @@ -1,4 +1,7 @@ edges +| testAlamofire.swift:150:45:150:45 | password : | testAlamofire.swift:150:13:150:45 | ... .+(_:_:) ... | +| testAlamofire.swift:152:51:152:51 | password : | testAlamofire.swift:152:19:152:51 | ... .+(_:_:) ... | +| testAlamofire.swift:154:38:154:38 | email : | testAlamofire.swift:154:14:154:46 | ... .+(_:_:) ... | | testSend.swift:41:10:41:18 | data : | testSend.swift:41:45:41:45 | data : | | testSend.swift:45:13:45:13 | password : | testSend.swift:52:27:52:27 | str1 | | testSend.swift:46:13:46:13 | password : | testSend.swift:53:27:53:27 | str2 | @@ -8,6 +11,12 @@ edges | testURL.swift:13:54:13:54 | passwd : | testURL.swift:13:22:13:54 | ... .+(_:_:) ... | | testURL.swift:16:55:16:55 | credit_card_no : | testURL.swift:16:22:16:55 | ... .+(_:_:) ... | nodes +| testAlamofire.swift:150:13:150:45 | ... .+(_:_:) ... | semmle.label | ... .+(_:_:) ... | +| testAlamofire.swift:150:45:150:45 | password : | semmle.label | password : | +| testAlamofire.swift:152:19:152:51 | ... .+(_:_:) ... | semmle.label | ... .+(_:_:) ... | +| testAlamofire.swift:152:51:152:51 | password : | semmle.label | password : | +| testAlamofire.swift:154:14:154:46 | ... .+(_:_:) ... | semmle.label | ... .+(_:_:) ... | +| testAlamofire.swift:154:38:154:38 | email : | semmle.label | email : | | testSend.swift:29:19:29:19 | passwordPlain | semmle.label | passwordPlain | | testSend.swift:41:10:41:18 | data : | semmle.label | data : | | testSend.swift:41:45:41:45 | data : | semmle.label | data : | @@ -26,6 +35,9 @@ nodes subpaths | testSend.swift:47:17:47:17 | password : | testSend.swift:41:10:41:18 | data : | testSend.swift:41:45:41:45 | data : | testSend.swift:47:13:47:25 | call to pad(_:) : | #select +| testAlamofire.swift:150:13:150:45 | ... .+(_:_:) ... | testAlamofire.swift:150:45:150:45 | password : | testAlamofire.swift:150:13:150:45 | ... .+(_:_:) ... | This operation transmits '... .+(_:_:) ...', which may contain unencrypted sensitive data from $@. | testAlamofire.swift:150:45:150:45 | password : | password | +| testAlamofire.swift:152:19:152:51 | ... .+(_:_:) ... | testAlamofire.swift:152:51:152:51 | password : | testAlamofire.swift:152:19:152:51 | ... .+(_:_:) ... | This operation transmits '... .+(_:_:) ...', which may contain unencrypted sensitive data from $@. | testAlamofire.swift:152:51:152:51 | password : | password | +| testAlamofire.swift:154:14:154:46 | ... .+(_:_:) ... | testAlamofire.swift:154:38:154:38 | email : | testAlamofire.swift:154:14:154:46 | ... .+(_:_:) ... | This operation transmits '... .+(_:_:) ...', which may contain unencrypted sensitive data from $@. | testAlamofire.swift:154:38:154:38 | email : | email | | testSend.swift:29:19:29:19 | passwordPlain | testSend.swift:29:19:29:19 | passwordPlain | testSend.swift:29:19:29:19 | passwordPlain | This operation transmits 'passwordPlain', which may contain unencrypted sensitive data from $@. | testSend.swift:29:19:29:19 | passwordPlain | passwordPlain | | testSend.swift:52:27:52:27 | str1 | testSend.swift:45:13:45:13 | password : | testSend.swift:52:27:52:27 | str1 | This operation transmits 'str1', which may contain unencrypted sensitive data from $@. | testSend.swift:45:13:45:13 | password : | password | | testSend.swift:53:27:53:27 | str2 | testSend.swift:46:13:46:13 | password : | testSend.swift:53:27:53:27 | str2 | This operation transmits 'str2', which may contain unencrypted sensitive data from $@. | testSend.swift:46:13:46:13 | password : | password | diff --git a/swift/ql/test/query-tests/Security/CWE-311/testAlamofire.swift b/swift/ql/test/query-tests/Security/CWE-311/testAlamofire.swift index 9056eebdbf1..3a50c2cf249 100644 --- a/swift/ql/test/query-tests/Security/CWE-311/testAlamofire.swift +++ b/swift/ql/test/query-tests/Security/CWE-311/testAlamofire.swift @@ -147,11 +147,11 @@ struct MyEncodable: Encodable { func test1(username: String, password: String, email: String, harmless: String) { // sensitive data in URL - AF.request("http://example.com/login?p=" + password) // BAD [NOT DETECTED] + AF.request("http://example.com/login?p=" + password) // BAD AF.request("http://example.com/login?h=" + harmless) // GOOD (not sensitive) - AF.streamRequest("http://example.com/login?p=" + password) // BAD [NOT DETECTED] + AF.streamRequest("http://example.com/login?p=" + password) // BAD AF.streamRequest("http://example.com/login?h=" + harmless) // GOOD (not sensitive) - AF.download("http://example.com/" + email + ".html") // BAD [NOT DETECTED] + AF.download("http://example.com/" + email + ".html") // BAD AF.download("http://example.com/" + harmless + ".html") // GOOD (not sensitive) // sensitive data in parameters From 887d1893e755c38c1f834b5e45126b707fb1c3e6 Mon Sep 17 00:00:00 2001 From: Geoffrey White <40627776+geoffw0@users.noreply.github.com> Date: Thu, 10 Nov 2022 15:51:02 +0000 Subject: [PATCH 3/5] Swift: Make ql-for-ql happy. --- swift/ql/lib/codeql/swift/elements/expr/ApplyExpr.qll | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/swift/ql/lib/codeql/swift/elements/expr/ApplyExpr.qll b/swift/ql/lib/codeql/swift/elements/expr/ApplyExpr.qll index cf11158df92..5c82dc32afa 100644 --- a/swift/ql/lib/codeql/swift/elements/expr/ApplyExpr.qll +++ b/swift/ql/lib/codeql/swift/elements/expr/ApplyExpr.qll @@ -23,7 +23,7 @@ class ApplyExpr extends Generated::ApplyExpr { * Gets the argument of this `ApplyExpr` called `label` (if any). */ final Argument getArgumentWithLabel(string label) { - result = getAnArgument() and + result = this.getAnArgument() and result.getLabel() = label } From 3e6eedec300bcd6b9f70961225d63704d3a0f220 Mon Sep 17 00:00:00 2001 From: Geoffrey White <40627776+geoffw0@users.noreply.github.com> Date: Mon, 14 Nov 2022 14:42:56 +0000 Subject: [PATCH 4/5] Swift: Fix test output after merge. --- .../Security/CWE-311/CleartextTransmission.expected | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/swift/ql/test/query-tests/Security/CWE-311/CleartextTransmission.expected b/swift/ql/test/query-tests/Security/CWE-311/CleartextTransmission.expected index 0ce9f3b10c7..3245aa68d4e 100644 --- a/swift/ql/test/query-tests/Security/CWE-311/CleartextTransmission.expected +++ b/swift/ql/test/query-tests/Security/CWE-311/CleartextTransmission.expected @@ -1,11 +1,11 @@ edges +| testAlamofire.swift:150:45:150:45 | password : | testAlamofire.swift:150:13:150:45 | ... .+(_:_:) ... | +| testAlamofire.swift:152:51:152:51 | password : | testAlamofire.swift:152:19:152:51 | ... .+(_:_:) ... | +| testAlamofire.swift:154:38:154:38 | email : | testAlamofire.swift:154:14:154:46 | ... .+(_:_:) ... | | testSend.swift:5:5:5:29 | [summary param] 0 in init(_:) : | file://:0:0:0:0 | [summary] to write: return (return) in init(_:) : | | testSend.swift:33:14:33:32 | call to init(_:) : | testSend.swift:37:19:37:19 | data2 | | testSend.swift:33:19:33:19 | passwordPlain : | testSend.swift:5:5:5:29 | [summary param] 0 in init(_:) : | | testSend.swift:33:19:33:19 | passwordPlain : | testSend.swift:33:14:33:32 | call to init(_:) : | -| testAlamofire.swift:150:45:150:45 | password : | testAlamofire.swift:150:13:150:45 | ... .+(_:_:) ... | -| testAlamofire.swift:152:51:152:51 | password : | testAlamofire.swift:152:19:152:51 | ... .+(_:_:) ... | -| testAlamofire.swift:154:38:154:38 | email : | testAlamofire.swift:154:14:154:46 | ... .+(_:_:) ... | | testSend.swift:41:10:41:18 | data : | testSend.swift:41:45:41:45 | data : | | testSend.swift:45:13:45:13 | password : | testSend.swift:52:27:52:27 | str1 | | testSend.swift:46:13:46:13 | password : | testSend.swift:53:27:53:27 | str2 | @@ -16,13 +16,13 @@ edges | testURL.swift:16:55:16:55 | credit_card_no : | testURL.swift:16:22:16:55 | ... .+(_:_:) ... | nodes | file://:0:0:0:0 | [summary] to write: return (return) in init(_:) : | semmle.label | [summary] to write: return (return) in init(_:) : | -| testSend.swift:5:5:5:29 | [summary param] 0 in init(_:) : | semmle.label | [summary param] 0 in init(_:) : | | testAlamofire.swift:150:13:150:45 | ... .+(_:_:) ... | semmle.label | ... .+(_:_:) ... | | testAlamofire.swift:150:45:150:45 | password : | semmle.label | password : | | testAlamofire.swift:152:19:152:51 | ... .+(_:_:) ... | semmle.label | ... .+(_:_:) ... | | testAlamofire.swift:152:51:152:51 | password : | semmle.label | password : | | testAlamofire.swift:154:14:154:46 | ... .+(_:_:) ... | semmle.label | ... .+(_:_:) ... | -| testAlamofire.swift:154:38:154:38 | email : | semmle.label | email : |= +| testAlamofire.swift:154:38:154:38 | email : | semmle.label | email : | +| testSend.swift:5:5:5:29 | [summary param] 0 in init(_:) : | semmle.label | [summary param] 0 in init(_:) : | | testSend.swift:29:19:29:19 | passwordPlain | semmle.label | passwordPlain | | testSend.swift:33:14:33:32 | call to init(_:) : | semmle.label | call to init(_:) : | | testSend.swift:33:19:33:19 | passwordPlain : | semmle.label | passwordPlain : | From 556d68aeed7bd6177c410c7badf472645de43ce8 Mon Sep 17 00:00:00 2001 From: Geoffrey White <40627776+geoffw0@users.noreply.github.com> Date: Wed, 23 Nov 2022 09:17:18 +0000 Subject: [PATCH 5/5] Update swift/ql/src/queries/Security/CWE-311/CleartextTransmission.ql Co-authored-by: Tony Torralba --- swift/ql/src/queries/Security/CWE-311/CleartextTransmission.ql | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/swift/ql/src/queries/Security/CWE-311/CleartextTransmission.ql b/swift/ql/src/queries/Security/CWE-311/CleartextTransmission.ql index 21c1c538462..04108a33d29 100644 --- a/swift/ql/src/queries/Security/CWE-311/CleartextTransmission.ql +++ b/swift/ql/src/queries/Security/CWE-311/CleartextTransmission.ql @@ -66,8 +66,7 @@ class AlamofireTransmitted extends Transmitted { fName.regexpMatch("(request|streamRequest|download)\\(.*") and ( call.getArgument(0).getExpr() = this or - call.getArgumentWithLabel("parameters").getExpr() = this or - call.getArgumentWithLabel("headers").getExpr() = this + call.getArgumentWithLabel(["headers", "parameters"]).getExpr() = this ) ) }