logstruct 0.1.3 → 0.1.4
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 +4 -4
- data/CHANGELOG.md +6 -2
- data/lib/log_struct/integrations/rack_error_handler/middleware.rb +54 -16
- data/lib/log_struct/integrations/rack_error_handler.rb +3 -3
- data/lib/log_struct/integrations.rb +25 -5
- data/lib/log_struct/railtie.rb +11 -2
- data/lib/log_struct/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 01556306744df98d8548bd788dbaee74c29c976207289e7da9da69b7d5e443ca
|
4
|
+
data.tar.gz: 2f8d6b1becb62c66b139ca071871beccafb35d42396e7fc552b8ba4401a93363
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c4f6d5ad7c72bf84e59fcb97ecb70fd3829ff6b681c923104e821e817d085ccad33e7b489232ea86a2e02bfff64ff06be6d314e8dbdf0e01ae82c7c26189c361
|
7
|
+
data.tar.gz: d69f5b0bb706273f5096503f12b7b05519fae03f4ff8d3664794e89c1a6929f42b82dcf5798e782b317f18985ad85067e1c144f1b3739e54876eca163c2ad9bf
|
data/CHANGELOG.md
CHANGED
@@ -5,10 +5,14 @@ All notable changes to this project will be documented in this file.
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
7
7
|
|
8
|
-
## [0.1.3] - 2025-10-11
|
9
|
-
|
10
8
|
### Changed
|
11
9
|
|
10
|
+
## [0.1.4] - 2025-10-13
|
11
|
+
|
12
|
+
- Improve rack spoof handling and split integration setup
|
13
|
+
|
14
|
+
## [0.1.3] - 2025-10-11
|
15
|
+
|
12
16
|
- **Fix**: Changed storage, queue name, and format fields from `String` to `Symbol` type to match Rails conventions
|
13
17
|
- Affected log types: ActiveStorage, CarrierWave, Shrine (storage field), ActiveJob, GoodJob (queue_name field), Request (format field)
|
14
18
|
- JSON logging now enabled for all test runs (both local and CI) to ensure tests catch production bugs
|
@@ -55,8 +55,14 @@ module LogStruct
|
|
55
55
|
def call(env)
|
56
56
|
return @app.call(env) unless LogStruct.enabled?
|
57
57
|
|
58
|
-
|
58
|
+
request = ::ActionDispatch::Request.new(env)
|
59
|
+
|
59
60
|
begin
|
61
|
+
# Trigger the same spoofing checks that ActionDispatch::RemoteIp performs after
|
62
|
+
# it is initialized in the middleware stack. We run this manually because we
|
63
|
+
# execute before that middleware and still want spoofing attacks to surface here.
|
64
|
+
perform_remote_ip_check!(request)
|
65
|
+
|
60
66
|
@app.call(env)
|
61
67
|
rescue ::ActionDispatch::RemoteIp::IpSpoofAttackError => ip_spoof_error
|
62
68
|
# Create a security log for IP spoofing
|
@@ -65,7 +71,7 @@ module LogStruct
|
|
65
71
|
http_method: env["REQUEST_METHOD"],
|
66
72
|
user_agent: env["HTTP_USER_AGENT"],
|
67
73
|
referer: env["HTTP_REFERER"],
|
68
|
-
request_id:
|
74
|
+
request_id: request.request_id,
|
69
75
|
message: ip_spoof_error.message,
|
70
76
|
client_ip: env["HTTP_CLIENT_IP"],
|
71
77
|
x_forwarded_for: env["HTTP_X_FORWARDED_FOR"],
|
@@ -74,16 +80,9 @@ module LogStruct
|
|
74
80
|
|
75
81
|
::Rails.logger.warn(security_log)
|
76
82
|
|
77
|
-
|
78
|
-
context = extract_request_context(env)
|
79
|
-
LogStruct.handle_exception(ip_spoof_error, source: Source::Security, context: context)
|
80
|
-
|
81
|
-
# If handle_exception raised an exception then Rails will deal with it (e.g. config.exceptions_app)
|
82
|
-
# If we are only logging or reporting these security errors, then return a default response
|
83
|
-
[FORBIDDEN_STATUS, IP_SPOOF_HEADERS, [IP_SPOOF_HTML]]
|
83
|
+
[FORBIDDEN_STATUS, IP_SPOOF_HEADERS.dup, [IP_SPOOF_HTML]]
|
84
84
|
rescue ::ActionController::InvalidAuthenticityToken => invalid_auth_token_error
|
85
85
|
# Create a security log for CSRF error
|
86
|
-
request = ::ActionDispatch::Request.new(env)
|
87
86
|
security_log = Log::Security::CSRFViolation.new(
|
88
87
|
path: request.path,
|
89
88
|
http_method: request.method,
|
@@ -97,15 +96,15 @@ module LogStruct
|
|
97
96
|
LogStruct.error(security_log)
|
98
97
|
|
99
98
|
# Report to error reporting service and/or re-raise
|
100
|
-
context = extract_request_context(env)
|
99
|
+
context = extract_request_context(env, request)
|
101
100
|
LogStruct.handle_exception(invalid_auth_token_error, source: Source::Security, context: context)
|
102
101
|
|
103
102
|
# If handle_exception raised an exception then Rails will deal with it (e.g. config.exceptions_app)
|
104
103
|
# If we are only logging or reporting these security errors, then return a default response
|
105
|
-
[FORBIDDEN_STATUS, CSRF_HEADERS, [CSRF_HTML]]
|
104
|
+
[FORBIDDEN_STATUS, CSRF_HEADERS.dup, [CSRF_HTML]]
|
106
105
|
rescue => error
|
107
106
|
# Extract request context for error reporting
|
108
|
-
context = extract_request_context(env)
|
107
|
+
context = extract_request_context(env, request)
|
109
108
|
|
110
109
|
# Create and log a structured exception with request context
|
111
110
|
exception_log = Log.from_exception(Source::Rails, error, context)
|
@@ -119,9 +118,22 @@ module LogStruct
|
|
119
118
|
|
120
119
|
private
|
121
120
|
|
122
|
-
sig { params(
|
123
|
-
def
|
124
|
-
|
121
|
+
sig { params(request: ::ActionDispatch::Request).void }
|
122
|
+
def perform_remote_ip_check!(request)
|
123
|
+
action_dispatch_config = ::Rails.application.config.action_dispatch
|
124
|
+
check_ip = action_dispatch_config.ip_spoofing_check
|
125
|
+
return unless check_ip
|
126
|
+
|
127
|
+
proxies = normalized_trusted_proxies(action_dispatch_config.trusted_proxies)
|
128
|
+
|
129
|
+
::ActionDispatch::RemoteIp::GetIp
|
130
|
+
.new(request, check_ip, proxies)
|
131
|
+
.to_s
|
132
|
+
end
|
133
|
+
|
134
|
+
sig { params(env: T::Hash[String, T.untyped], request: T.nilable(::ActionDispatch::Request)).returns(T::Hash[Symbol, T.untyped]) }
|
135
|
+
def extract_request_context(env, request = nil)
|
136
|
+
request ||= ::ActionDispatch::Request.new(env)
|
125
137
|
{
|
126
138
|
request_id: request.request_id,
|
127
139
|
path: request.path,
|
@@ -133,6 +145,32 @@ module LogStruct
|
|
133
145
|
# If we can't extract request context, return minimal info
|
134
146
|
{error_extracting_context: error.message}
|
135
147
|
end
|
148
|
+
|
149
|
+
sig { params(configured_proxies: T.untyped).returns(T.untyped) }
|
150
|
+
def normalized_trusted_proxies(configured_proxies)
|
151
|
+
if configured_proxies.nil? || (configured_proxies.respond_to?(:empty?) && configured_proxies.empty?)
|
152
|
+
return ::ActionDispatch::RemoteIp::TRUSTED_PROXIES
|
153
|
+
end
|
154
|
+
|
155
|
+
return configured_proxies if configured_proxies.respond_to?(:any?)
|
156
|
+
|
157
|
+
raise(
|
158
|
+
ArgumentError,
|
159
|
+
<<~EOM
|
160
|
+
Setting config.action_dispatch.trusted_proxies to a single value isn't
|
161
|
+
supported. Please set this to an enumerable instead. For
|
162
|
+
example, instead of:
|
163
|
+
|
164
|
+
config.action_dispatch.trusted_proxies = IPAddr.new("10.0.0.0/8")
|
165
|
+
|
166
|
+
Wrap the value in an Array:
|
167
|
+
|
168
|
+
config.action_dispatch.trusted_proxies = [IPAddr.new("10.0.0.0/8")]
|
169
|
+
|
170
|
+
Note that passing an enumerable will *replace* the default set of trusted proxies.
|
171
|
+
EOM
|
172
|
+
)
|
173
|
+
end
|
136
174
|
end
|
137
175
|
end
|
138
176
|
end
|
@@ -19,9 +19,9 @@ module LogStruct
|
|
19
19
|
return nil unless config.integrations.enable_rack_error_handler
|
20
20
|
|
21
21
|
# Add structured logging middleware for security violations and errors
|
22
|
-
# Need to insert
|
23
|
-
::Rails.application.middleware.
|
24
|
-
::ActionDispatch::
|
22
|
+
# Need to insert before RemoteIp to catch IP spoofing errors it raises
|
23
|
+
::Rails.application.middleware.insert_before(
|
24
|
+
::ActionDispatch::RemoteIp,
|
25
25
|
Integrations::RackErrorHandler::Middleware
|
26
26
|
)
|
27
27
|
|
@@ -38,11 +38,25 @@ module LogStruct
|
|
38
38
|
end
|
39
39
|
end
|
40
40
|
|
41
|
-
sig { void }
|
42
|
-
def self.setup_integrations
|
41
|
+
sig { params(stage: Symbol).void }
|
42
|
+
def self.setup_integrations(stage: :all)
|
43
43
|
config = LogStruct.config
|
44
44
|
|
45
|
-
|
45
|
+
case stage
|
46
|
+
when :non_middleware
|
47
|
+
setup_non_middleware_integrations(config)
|
48
|
+
when :middleware
|
49
|
+
setup_middleware_integrations(config)
|
50
|
+
when :all
|
51
|
+
setup_non_middleware_integrations(config)
|
52
|
+
setup_middleware_integrations(config)
|
53
|
+
else
|
54
|
+
raise ArgumentError, "Unknown integration stage: #{stage}"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
sig { params(config: LogStruct::Configuration).void }
|
59
|
+
def self.setup_non_middleware_integrations(config)
|
46
60
|
Integrations::Lograge.setup(config) if config.integrations.enable_lograge
|
47
61
|
Integrations::ActionMailer.setup(config) if config.integrations.enable_actionmailer
|
48
62
|
Integrations::ActiveJob.setup(config) if config.integrations.enable_activejob
|
@@ -51,8 +65,6 @@ module LogStruct
|
|
51
65
|
Integrations::GoodJob.setup(config) if config.integrations.enable_goodjob
|
52
66
|
Integrations::Ahoy.setup(config) if config.integrations.enable_ahoy
|
53
67
|
Integrations::ActiveModelSerializers.setup(config) if config.integrations.enable_active_model_serializers
|
54
|
-
Integrations::HostAuthorization.setup(config) if config.integrations.enable_host_authorization
|
55
|
-
Integrations::RackErrorHandler.setup(config) if config.integrations.enable_rack_error_handler
|
56
68
|
Integrations::Shrine.setup(config) if config.integrations.enable_shrine
|
57
69
|
Integrations::ActiveStorage.setup(config) if config.integrations.enable_activestorage
|
58
70
|
Integrations::CarrierWave.setup(config) if config.integrations.enable_carrierwave
|
@@ -62,5 +74,13 @@ module LogStruct
|
|
62
74
|
end
|
63
75
|
Integrations::Puma.setup(config) if config.integrations.enable_puma
|
64
76
|
end
|
77
|
+
|
78
|
+
sig { params(config: LogStruct::Configuration).void }
|
79
|
+
def self.setup_middleware_integrations(config)
|
80
|
+
Integrations::HostAuthorization.setup(config) if config.integrations.enable_host_authorization
|
81
|
+
Integrations::RackErrorHandler.setup(config) if config.integrations.enable_rack_error_handler
|
82
|
+
end
|
83
|
+
|
84
|
+
private_class_method :setup_non_middleware_integrations, :setup_middleware_integrations
|
65
85
|
end
|
66
86
|
end
|
data/lib/log_struct/railtie.rb
CHANGED
@@ -25,12 +25,21 @@ module LogStruct
|
|
25
25
|
# Merge Rails filter parameters into our filters
|
26
26
|
LogStruct.merge_rails_filter_parameters!
|
27
27
|
|
28
|
-
# Set up
|
29
|
-
Integrations.setup_integrations
|
28
|
+
# Set up non-middleware integrations first
|
29
|
+
Integrations.setup_integrations(stage: :non_middleware)
|
30
30
|
|
31
31
|
# Note: Host allowances are managed by the test app itself.
|
32
32
|
end
|
33
33
|
|
34
|
+
# Setup middleware integrations during Rails configuration (before middleware stack is built)
|
35
|
+
# Must be done in the Railtie class body, not in an initializer
|
36
|
+
initializer "logstruct.configure_middleware", before: :build_middleware_stack do |app|
|
37
|
+
# This runs before middleware stack is frozen, so we can configure it
|
38
|
+
next unless LogStruct.enabled?
|
39
|
+
|
40
|
+
Integrations.setup_integrations(stage: :middleware)
|
41
|
+
end
|
42
|
+
|
34
43
|
# Emit Puma lifecycle logs when running `rails server`
|
35
44
|
initializer "logstruct.puma_lifecycle", after: "logstruct.configure_logger" do
|
36
45
|
is_server = ::LogStruct.server_mode?
|
data/lib/log_struct/version.rb
CHANGED