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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 77b4f5cd95c84bd5b418bb95b1856af146e70a64e6d39e3a303e33e083faa64d
4
- data.tar.gz: 670c515b7eb8fbe5320c3bc52e2a68234aff31f8633fc071286a1b4a816132b6
3
+ metadata.gz: 01556306744df98d8548bd788dbaee74c29c976207289e7da9da69b7d5e443ca
4
+ data.tar.gz: 2f8d6b1becb62c66b139ca071871beccafb35d42396e7fc552b8ba4401a93363
5
5
  SHA512:
6
- metadata.gz: f7b81a7d6893f7db9b51c71d5623beedfa75d81e5c9c1ec51003db37a5873018427e81a86aaa2de3a73741cb372bea2d859980b96695592ee9249ea0def0fe9d
7
- data.tar.gz: e8fd117ec5795acebba1a996d2777f25f2ac89afdbb4fa76cb18ec1faa5fea11800b9cf452178412eb0fabe77ca4bcc223dd516d53b1217031f7433361bdc41c
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
- # Try to process the request
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: env["action_dispatch.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
- # Report the error
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(env: T::Hash[String, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
123
- def extract_request_context(env)
124
- request = ::ActionDispatch::Request.new(env)
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 after ShowExceptions to catch IP spoofing errors
23
- ::Rails.application.middleware.insert_after(
24
- ::ActionDispatch::ShowExceptions,
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
- # Set up each integration with consistent configuration pattern
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
@@ -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 all integrations
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?
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module LogStruct
5
- VERSION = "0.1.3"
5
+ VERSION = "0.1.4"
6
6
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: logstruct
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - DocSpring