ree_lib 1.3.15 → 1.3.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7b6bab291292300f6d1067e79262021b1a1ce40474fd4d3baf2ddec2398c521f
|
|
4
|
+
data.tar.gz: fe0536bc647d3bd48cbc04197a61fc5415aa8c58fb0aa1578eb90eafaa1c424c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 40ec71033ebe98320f43b20cf2dd626272cc313ee3d28540fd333d8aabf403f38c87c502c174184331aa6ca7e44cc6b657e5854a740f75eff844c6e9ab2f64b7
|
|
7
|
+
data.tar.gz: 03433f0fd291d8709afd41856ed2141e1b418ee39d72ce19148390c86660a7a13411e99f20e0d2a98dac5d37d96fbf05fba845f0ba8b6ae7e1c395e074fc381a
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [1.3.16] - 2026-10-21
|
|
4
|
+
|
|
5
|
+
- `ree_roda`: the `ree_logger` plugin applies `filter_params` to the query
|
|
6
|
+
string of the `Request:` line. It used to be logged verbatim, so a secret
|
|
7
|
+
sent in the URL reached the log even though `Params` showed it filtered.
|
|
8
|
+
A bracketed name (`user[password]`, also percent-encoded) is matched by
|
|
9
|
+
each of its segments.
|
|
10
|
+
- `ree_roda`: `filter_params` in the `ree_logger` plugin now walks nested
|
|
11
|
+
hashes and arrays; it used to check top level keys only. Names are still
|
|
12
|
+
matched exactly.
|
|
13
|
+
|
|
3
14
|
## [1.3.12] - 2026-09-02
|
|
4
15
|
|
|
5
16
|
- `ree_audit`: `annotate_audit` fn — the entry point product code uses to attach
|
data/Gemfile.lock
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
require "uri"
|
|
1
2
|
package_require "ree_logger/beans/logger"
|
|
2
3
|
|
|
3
4
|
class Roda
|
|
@@ -50,7 +51,7 @@ class Roda
|
|
|
50
51
|
|
|
51
52
|
message = <<~DOC
|
|
52
53
|
Request/Response details:
|
|
53
|
-
Request: #{env["REQUEST_METHOD"]} #{env
|
|
54
|
+
Request: #{env["REQUEST_METHOD"]} #{_ree_logger_request_path(env)} #{opts[:ree_logger_log_params] ? "\n Params: " + _ree_logger_filtered_params.inspect : ""}
|
|
54
55
|
Response status: #{response.status || "-"}#{(400..499).include?(response.status) ? "\n Response body: " + response.body[0].to_s : ""}
|
|
55
56
|
Time Taken: #{elapsed_time}
|
|
56
57
|
DOC
|
|
@@ -64,11 +65,69 @@ class Roda
|
|
|
64
65
|
|
|
65
66
|
return params if filter_keys.empty?
|
|
66
67
|
|
|
68
|
+
_ree_logger_filter_value(params, filter_keys, opts[:ree_logger_filter_params_placeholder])
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Keys are matched by exact name at any depth: a password nested in
|
|
72
|
+
# `{user: {password: ...}}` is still a password. Exact match, unlike the
|
|
73
|
+
# substring match of ree_audit, keeps `token_id` and the like readable.
|
|
74
|
+
def _ree_logger_filter_value(value, filter_keys, placeholder)
|
|
75
|
+
case value
|
|
76
|
+
when Hash
|
|
77
|
+
value.each_with_object({}) do |(k, v), h|
|
|
78
|
+
h[k] = if filter_keys.include?(k.to_s)
|
|
79
|
+
placeholder
|
|
80
|
+
else
|
|
81
|
+
_ree_logger_filter_value(v, filter_keys, placeholder)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
when Array
|
|
85
|
+
value.map { _ree_logger_filter_value(_1, filter_keys, placeholder) }
|
|
86
|
+
else
|
|
87
|
+
value
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def _ree_logger_request_path(env)
|
|
92
|
+
path = env["SCRIPT_NAME"].to_s + env["PATH_INFO"].to_s
|
|
93
|
+
query = env["QUERY_STRING"]
|
|
94
|
+
|
|
95
|
+
return path if query.nil? || query.empty?
|
|
96
|
+
|
|
97
|
+
"#{path}?#{_ree_logger_filtered_query(query)}"
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# The query string is logged verbatim next to the path, so without this
|
|
101
|
+
# a secret sent in the URL (OAuth callback code, a token from an email
|
|
102
|
+
# link) reached the log even though Params showed it as filtered.
|
|
103
|
+
def _ree_logger_filtered_query(query)
|
|
104
|
+
filter_keys = opts[:ree_logger_filter_params]
|
|
105
|
+
|
|
106
|
+
return query if filter_keys.empty?
|
|
107
|
+
|
|
67
108
|
placeholder = opts[:ree_logger_filter_params_placeholder]
|
|
68
109
|
|
|
69
|
-
|
|
70
|
-
|
|
110
|
+
query.split("&", -1).map do |pair|
|
|
111
|
+
key, value = pair.split("=", 2)
|
|
112
|
+
|
|
113
|
+
if value && _ree_logger_filtered_query_key?(key, filter_keys)
|
|
114
|
+
"#{key}=#{placeholder}"
|
|
115
|
+
else
|
|
116
|
+
pair
|
|
117
|
+
end
|
|
118
|
+
end.join("&")
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# `user[password]=...` is the nested form of the same param, so every
|
|
122
|
+
# bracket segment of the name is checked.
|
|
123
|
+
def _ree_logger_filtered_query_key?(key, filter_keys)
|
|
124
|
+
name = begin
|
|
125
|
+
URI.decode_www_form_component(key)
|
|
126
|
+
rescue ArgumentError
|
|
127
|
+
key
|
|
71
128
|
end
|
|
129
|
+
|
|
130
|
+
name.scan(/[^\[\]]+/).any? { filter_keys.include?(_1) }
|
|
72
131
|
end
|
|
73
132
|
|
|
74
133
|
# Create timer instance used for timing
|
|
@@ -3,34 +3,107 @@
|
|
|
3
3
|
package_require("ree_roda/plugins/ree_logger")
|
|
4
4
|
|
|
5
5
|
RSpec.describe :ree_logger do
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
class TestFilterLogger
|
|
7
|
+
def info(msg)
|
|
8
|
+
logs << msg
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def logs
|
|
12
|
+
@logs ||= []
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
let(:logger) { TestFilterLogger.new }
|
|
17
|
+
|
|
18
|
+
let(:app) {
|
|
19
|
+
log = logger
|
|
20
|
+
|
|
21
|
+
Class.new(ReeRoda::App) do
|
|
22
|
+
plugin(:ree_logger, ree_logger: log, filter_params: ["password", "token"])
|
|
23
|
+
|
|
8
24
|
route do |r|
|
|
9
|
-
r.
|
|
25
|
+
r.is "health" do
|
|
10
26
|
"success"
|
|
11
27
|
end
|
|
12
|
-
|
|
13
|
-
r.ree_routes
|
|
14
28
|
end
|
|
15
29
|
end
|
|
30
|
+
}
|
|
16
31
|
|
|
32
|
+
def request_line
|
|
33
|
+
logger.logs.first.lines.find { _1.include?("Request:") }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def params_line
|
|
37
|
+
logger.logs.first.lines.find { _1.include?("Params:") }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it "filters request params" do
|
|
17
41
|
env = { "REQUEST_METHOD" => "GET", "PATH_INFO" => "/health", "QUERY_STRING" => "email=test@example.com&password=password" }
|
|
18
42
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
43
|
+
app.app.call(env)
|
|
44
|
+
|
|
45
|
+
expect(logger.logs.first).to match('"password" => "\[FILTERED\]"')
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it "filters secrets in the query string of the request line" do
|
|
49
|
+
env = { "REQUEST_METHOD" => "GET", "PATH_INFO" => "/health", "QUERY_STRING" => "email=test%40example.com&password=s3cret&token=tok-1" }
|
|
50
|
+
|
|
51
|
+
app.app.call(env)
|
|
24
52
|
|
|
25
|
-
|
|
26
|
-
|
|
53
|
+
expect(request_line).to include("/health?email=test%40example.com&password=[FILTERED]&token=[FILTERED]")
|
|
54
|
+
expect(logger.logs.first).not_to include("s3cret")
|
|
55
|
+
expect(logger.logs.first).not_to include("tok-1")
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it "filters bracketed and encoded names in the query string" do
|
|
59
|
+
env = { "REQUEST_METHOD" => "GET", "PATH_INFO" => "/health", "QUERY_STRING" => "user%5Bpassword%5D=s3cret&user[name]=bob&token_id=7" }
|
|
60
|
+
|
|
61
|
+
app.app.call(env)
|
|
62
|
+
|
|
63
|
+
expect(request_line).to include("user%5Bpassword%5D=[FILTERED]&user[name]=bob&token_id=7")
|
|
64
|
+
expect(logger.logs.first).not_to include("s3cret")
|
|
65
|
+
expect(params_line).to include('"token_id" => "7"')
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
it "keeps an empty query string out of the request line" do
|
|
69
|
+
env = { "REQUEST_METHOD" => "GET", "PATH_INFO" => "/health", "QUERY_STRING" => "" }
|
|
70
|
+
|
|
71
|
+
app.app.call(env)
|
|
72
|
+
|
|
73
|
+
expect(request_line).to include("Request: GET /health \n")
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it "filters nested params of a JSON body" do
|
|
77
|
+
body = '{"user":{"name":"bob","password":"s3cret"},"sessions":[{"token":"tok-1","id":1}]}'
|
|
78
|
+
env = {
|
|
79
|
+
"REQUEST_METHOD" => "POST", "PATH_INFO" => "/health",
|
|
80
|
+
"CONTENT_TYPE" => "application/json", "rack.input" => StringIO.new(body)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
app.app.call(env)
|
|
84
|
+
|
|
85
|
+
expect(params_line).to include('"name" => "bob"')
|
|
86
|
+
expect(params_line).to include('"id" => 1')
|
|
87
|
+
expect(logger.logs.first).not_to include("s3cret")
|
|
88
|
+
expect(logger.logs.first).not_to include("tok-1")
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
it "logs the query string untouched without filter params" do
|
|
92
|
+
log = logger
|
|
93
|
+
plain_app = Class.new(ReeRoda::App) do
|
|
94
|
+
plugin(:ree_logger, ree_logger: log)
|
|
95
|
+
|
|
96
|
+
route do |r|
|
|
97
|
+
r.is "health" do
|
|
98
|
+
"success"
|
|
99
|
+
end
|
|
27
100
|
end
|
|
28
101
|
end
|
|
29
|
-
logger = TestFilterLogger.new
|
|
30
102
|
|
|
31
|
-
|
|
32
|
-
TestFilterApp.app.call(env)
|
|
103
|
+
env = { "REQUEST_METHOD" => "GET", "PATH_INFO" => "/health", "QUERY_STRING" => "password=p" }
|
|
33
104
|
|
|
34
|
-
|
|
105
|
+
plain_app.app.call(env)
|
|
106
|
+
|
|
107
|
+
expect(request_line).to include("/health?password=p")
|
|
35
108
|
end
|
|
36
|
-
end
|
|
109
|
+
end
|
data/lib/ree_lib/version.rb
CHANGED