diff --git a/Gemfile b/Gemfile index 6e851f7c..2e542018 100644 --- a/Gemfile +++ b/Gemfile @@ -6,7 +6,7 @@ gemspec group :development do gem "irb", "~> 1" - gem "ostruct", "~> 0.6.1" + gem "ostruct", "~> 0.6.2" gem "rack-test", "~> 2.2" gem "rspec", "~> 3" gem "rubocop", "~> 1" diff --git a/Gemfile.lock b/Gemfile.lock index 1aa05460..ba9b921d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - hooks-ruby (0.6.0) + hooks-ruby (0.6.1) dry-schema (~> 1.14, >= 1.14.1) grape (~> 2.3) puma (~> 6.6) @@ -95,7 +95,7 @@ GEM net-http (0.6.0) uri nio4r (2.7.4) - ostruct (0.6.1) + ostruct (0.6.2) parallel (1.27.0) parser (3.3.8.0) ast (~> 2.4.1) @@ -125,7 +125,7 @@ GEM rack-test (2.2.0) rack (>= 1.3) rainbow (3.1.1) - rdoc (6.14.0) + rdoc (6.14.1) erb psych (>= 4.0.0) redacting-logger (1.5.0) @@ -139,7 +139,7 @@ GEM rspec-core (~> 3.13.0) rspec-expectations (~> 3.13.0) rspec-mocks (~> 3.13.0) - rspec-core (3.13.4) + rspec-core (3.13.5) rspec-support (~> 3.13.0) rspec-expectations (3.13.5) diff-lcs (>= 1.2.0, < 2.0) @@ -148,7 +148,7 @@ GEM diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) rspec-support (3.13.4) - rubocop (1.76.2) + rubocop (1.77.0) json (~> 2.3) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.1.0) @@ -222,7 +222,7 @@ PLATFORMS DEPENDENCIES hooks-ruby! irb (~> 1) - ostruct (~> 0.6.1) + ostruct (~> 0.6.2) rack-test (~> 2.2) rspec (~> 3) rubocop (~> 1) diff --git a/lib/hooks/plugins/auth/hmac.rb b/lib/hooks/plugins/auth/hmac.rb index e3d5d3d1..92225417 100644 --- a/lib/hooks/plugins/auth/hmac.rb +++ b/lib/hooks/plugins/auth/hmac.rb @@ -127,7 +127,7 @@ def self.valid?(payload:, headers:, config:) end # Validate signature format using shared validation but with HMAC-specific length limit - return false unless validate_signature_format(raw_signature) + return false unless validate_signature_format?(raw_signature) # Now we can safely normalize headers for the rest of the validation normalized_headers = normalize_headers(headers) @@ -192,7 +192,7 @@ def self.valid?(payload:, headers:, config:) # @param signature [String] Raw signature to validate # @return [Boolean] true if signature is valid # @api private - def self.validate_signature_format(signature) + def self.validate_signature_format?(signature) # Check signature length with HMAC-specific limit if signature.length > MAX_SIGNATURE_LENGTH log.warn("Auth::HMAC validation failed: Signature length exceeds maximum limit of #{MAX_SIGNATURE_LENGTH} characters") diff --git a/lib/hooks/version.rb b/lib/hooks/version.rb index a38b3d23..1b75120d 100644 --- a/lib/hooks/version.rb +++ b/lib/hooks/version.rb @@ -4,5 +4,5 @@ module Hooks # Current version of the Hooks webhook framework # @return [String] The version string following semantic versioning - VERSION = "0.6.0".freeze + VERSION = "0.6.1".freeze end diff --git a/spec/unit/lib/hooks/plugins/auth/hmac_spec.rb b/spec/unit/lib/hooks/plugins/auth/hmac_spec.rb index 0d321cfb..a78781de 100644 --- a/spec/unit/lib/hooks/plugins/auth/hmac_spec.rb +++ b/spec/unit/lib/hooks/plugins/auth/hmac_spec.rb @@ -27,7 +27,7 @@ allow(ENV).to receive(:[]).with("HMAC_TEST_SECRET").and_return(secret) end - def valid_with(args = {}) + def valid_with?(args = {}) args = { config: default_config }.merge(args) described_class.valid?(payload:, **args) end @@ -55,31 +55,31 @@ def create_timestamped_signature(timestamp, version = "v0") let(:headers) { { default_header => signature } } it "returns true for a valid signature" do - expect(valid_with(headers:)).to be true + expect(valid_with?(headers:)).to be true end it "returns false for an invalid signature" do bad_headers = { default_header => "sha256=bad" } - expect(valid_with(headers: bad_headers)).to be false + expect(valid_with?(headers: bad_headers)).to be false end it "returns false if signature header is missing" do - expect(valid_with(headers: {})).to be false + expect(valid_with?(headers: {})).to be false end it "returns false if secret is nil or empty" do # Test nil secret via environment variable allow(ENV).to receive(:[]).with("HMAC_TEST_SECRET").and_return(nil) - expect(valid_with(headers:)).to be false + expect(valid_with?(headers:)).to be false # Test empty secret via environment variable allow(ENV).to receive(:[]).with("HMAC_TEST_SECRET").and_return("") - expect(valid_with(headers:)).to be false + expect(valid_with?(headers:)).to be false end it "normalizes header names to lowercase" do upcase_headers = { default_header.upcase => signature } - expect(valid_with(headers: upcase_headers)).to be true + expect(valid_with?(headers: upcase_headers)).to be true end end @@ -99,12 +99,12 @@ def create_timestamped_signature(timestamp, version = "v0") let(:headers) { { header => signature } } it "returns true for a valid hash-only signature" do - expect(valid_with(headers:, config:)).to be true + expect(valid_with?(headers:, config:)).to be true end it "returns false for an invalid hash-only signature" do bad_headers = { header => "bad" } - expect(valid_with(headers: bad_headers, config:)).to be false + expect(valid_with?(headers: bad_headers, config:)).to be false end end @@ -130,24 +130,24 @@ def create_timestamped_signature(timestamp, version = "v0") end it "returns true for a valid versioned signature with valid timestamp" do - expect(valid_with(headers:, config:)).to be true + expect(valid_with?(headers:, config:)).to be true end it "returns false for an expired timestamp" do old_timestamp = (Time.now.to_i - 1000).to_s old_signature = create_timestamped_signature(old_timestamp) bad_headers = { header => old_signature, timestamp_header => old_timestamp } - expect(valid_with(headers: bad_headers, config:)).to be false + expect(valid_with?(headers: bad_headers, config:)).to be false end it "returns false if timestamp header is missing" do bad_headers = { header => signature } - expect(valid_with(headers: bad_headers, config:)).to be false + expect(valid_with?(headers: bad_headers, config:)).to be false end it "returns false if timestamp is not an integer string" do bad_headers = { header => signature, timestamp_header => "notanumber" } - expect(valid_with(headers: bad_headers, config:)).to be false + expect(valid_with?(headers: bad_headers, config:)).to be false end end @@ -166,7 +166,7 @@ def create_timestamped_signature(timestamp, version = "v0") let(:headers) { { header => signature } } it "returns false for unsupported algorithm" do - expect(valid_with(headers:, config:)).to be false + expect(valid_with?(headers:, config:)).to be false end end @@ -175,7 +175,7 @@ def create_timestamped_signature(timestamp, version = "v0") let(:config) { { auth: { secret_env_key: "HMAC_TEST_SECRET" } } } it "uses defaults and validates correctly" do - expect(valid_with(headers:, config:)).to be true + expect(valid_with?(headers:, config:)).to be true end end @@ -185,14 +185,14 @@ def create_timestamped_signature(timestamp, version = "v0") let(:tampered_payload) { '{"foo":"evil"}' } it "returns false if payload does not match signature" do - expect(valid_with(payload: tampered_payload, headers:)).to be false + expect(valid_with?(payload: tampered_payload, headers:)).to be false end end context "with nil headers" do let(:headers) { nil } it "returns false" do - expect(valid_with(headers:)).to be false + expect(valid_with?(headers:)).to be false end end @@ -200,7 +200,7 @@ def create_timestamped_signature(timestamp, version = "v0") let(:headers) { { default_header => "sha256=bad" } } let(:config) { { not_validator: true } } it "returns false" do - expect(valid_with(headers:, config:)).to be false + expect(valid_with?(headers:, config:)).to be false end end @@ -210,72 +210,72 @@ def create_timestamped_signature(timestamp, version = "v0") it "returns false for empty signature header value" do empty_headers = { default_header => "" } - expect(valid_with(headers: empty_headers)).to be false + expect(valid_with?(headers: empty_headers)).to be false end it "returns false for whitespace-only signature" do whitespace_headers = { default_header => " " } - expect(valid_with(headers: whitespace_headers)).to be false + expect(valid_with?(headers: whitespace_headers)).to be false end it "returns false for signature with only algorithm prefix" do incomplete_headers = { default_header => "sha256=" } - expect(valid_with(headers: incomplete_headers)).to be false + expect(valid_with?(headers: incomplete_headers)).to be false end it "returns false for malformed signature format" do malformed_headers = { default_header => "sha256" } - expect(valid_with(headers: malformed_headers)).to be false + expect(valid_with?(headers: malformed_headers)).to be false end it "returns false for signature with wrong algorithm prefix" do wrong_algo_headers = { default_header => create_algorithm_prefixed_signature(payload, "sha1") } - expect(valid_with(headers: wrong_algo_headers)).to be false + expect(valid_with?(headers: wrong_algo_headers)).to be false end it "returns false for signature with extra characters" do tampered_headers = { default_header => signature + "extra" } - expect(valid_with(headers: tampered_headers)).to be false + expect(valid_with?(headers: tampered_headers)).to be false end it "returns false for signature with leading/trailing whitespace" do whitespace_headers = { default_header => " #{signature} " } - expect(valid_with(headers: whitespace_headers)).to be false + expect(valid_with?(headers: whitespace_headers)).to be false end it "returns false for case-sensitive signature tampering" do case_tampered = signature.upcase case_headers = { default_header => case_tampered } - expect(valid_with(headers: case_headers)).to be false + expect(valid_with?(headers: case_headers)).to be false end it "returns false for null byte injection in signature" do null_byte_headers = { default_header => signature + "\x00" } - expect(valid_with(headers: null_byte_headers)).to be false + expect(valid_with?(headers: null_byte_headers)).to be false end it "returns false for unicode normalization attacks" do # Using similar-looking unicode characters unicode_headers = { default_header => signature.gsub("a", "а") } # Cyrillic 'a' - expect(valid_with(headers: unicode_headers)).to be false + expect(valid_with?(headers: unicode_headers)).to be false end it "returns false when secret contains null bytes" do null_secret = "secret\x00injection" allow(ENV).to receive(:[]).with("HMAC_TEST_SECRET").and_return(null_secret) - expect(valid_with(headers:)).to be false + expect(valid_with?(headers:)).to be false end it "returns false when payload is modified with invisible characters" do invisible_payload = payload + "\u200b" # Zero-width space - expect(valid_with(payload: invisible_payload, headers:)).to be false + expect(valid_with?(payload: invisible_payload, headers:)).to be false end it "handles very long signatures gracefully" do long_signature = "sha256=" + ("a" * 10000) long_headers = { default_header => long_signature } expect(log).to receive(:warn).with(/Signature length exceeds maximum limit/) - expect(valid_with(headers: long_headers)).to be false + expect(valid_with?(headers: long_headers)).to be false end it "returns false for signatures exceeding maximum length limit" do @@ -283,14 +283,14 @@ def create_timestamped_signature(timestamp, version = "v0") oversized_signature = "sha256=" + ("a" * (1024 - 7 + 1)) # -7 for "sha256=" prefix oversized_headers = { default_header => oversized_signature } expect(log).to receive(:warn).with(/Signature length exceeds maximum limit/) - expect(valid_with(headers: oversized_headers)).to be false + expect(valid_with?(headers: oversized_headers)).to be false end it "handles very long payloads" do long_payload = "a" * 100000 long_signature = create_algorithm_prefixed_signature(long_payload) long_headers = { default_header => long_signature } - expect(valid_with(payload: long_payload, headers: long_headers)).to be true + expect(valid_with?(payload: long_payload, headers: long_headers)).to be true end it "returns false for payloads exceeding maximum size limit" do @@ -299,14 +299,14 @@ def create_timestamped_signature(timestamp, version = "v0") signature = create_algorithm_prefixed_signature(payload) # Use regular payload for signature headers_with_signature = { default_header => signature } expect(log).to receive(:warn).with(/Payload size exceeds maximum limit/) - expect(valid_with(payload: oversized_payload, headers: headers_with_signature)).to be false + expect(valid_with?(payload: oversized_payload, headers: headers_with_signature)).to be false end it "returns false and logs for signature containing non-null control characters" do control_char = "\x01" headers_with_control = { default_header => signature + control_char } expect(log).to receive(:warn).with(/control characters/) - expect(valid_with(headers: headers_with_control)).to be false + expect(valid_with?(headers: headers_with_control)).to be false end end @@ -327,7 +327,7 @@ def create_timestamped_signature(timestamp, version = "v0") hash_only_sig = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha256"), secret, base_payload) attacker_headers = { "X-Signature" => hash_only_sig } - expect(valid_with(payload: base_payload, headers: attacker_headers, config: server_config)).to be false + expect(valid_with?(payload: base_payload, headers: attacker_headers, config: server_config)).to be false end it "fails when server expects hash-only but receives algorithm-prefixed" do @@ -344,7 +344,7 @@ def create_timestamped_signature(timestamp, version = "v0") algo_prefixed_sig = "sha256=" + OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha256"), secret, base_payload) attacker_headers = { "X-Signature" => algo_prefixed_sig } - expect(valid_with(payload: base_payload, headers: attacker_headers, config: server_config)).to be false + expect(valid_with?(payload: base_payload, headers: attacker_headers, config: server_config)).to be false end it "fails when server expects version-prefixed but receives algorithm-prefixed" do @@ -369,7 +369,7 @@ def create_timestamped_signature(timestamp, version = "v0") "X-Timestamp" => timestamp } - expect(valid_with(payload: base_payload, headers: attacker_headers, config: server_config)).to be false + expect(valid_with?(payload: base_payload, headers: attacker_headers, config: server_config)).to be false end it "fails when server expects algorithm-prefixed but receives version-prefixed" do @@ -388,7 +388,7 @@ def create_timestamped_signature(timestamp, version = "v0") "X-Signature" => versioned_sig, "X-Timestamp" => timestamp } - expect(valid_with(payload: base_payload, headers: attacker_headers, config: server_config)).to be false + expect(valid_with?(payload: base_payload, headers: attacker_headers, config: server_config)).to be false end it "fails when algorithm in config differs from signature prefix" do @@ -405,7 +405,7 @@ def create_timestamped_signature(timestamp, version = "v0") sha256_sig = "sha256=" + OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha256"), secret, base_payload) attacker_headers = { "X-Signature" => sha256_sig } - expect(valid_with(payload: base_payload, headers: attacker_headers, config: server_config)).to be false + expect(valid_with?(payload: base_payload, headers: attacker_headers, config: server_config)).to be false end it "fails when version prefix in config differs from signature" do @@ -432,7 +432,7 @@ def create_timestamped_signature(timestamp, version = "v0") "X-Timestamp" => timestamp } - expect(valid_with(payload: base_payload, headers: attacker_headers, config: server_config)).to be false + expect(valid_with?(payload: base_payload, headers: attacker_headers, config: server_config)).to be false end end @@ -458,7 +458,7 @@ def create_timestamped_signature(timestamp, version = "v0") def test_invalid_timestamp(invalid_timestamp, description) signature = create_timestamped_signature(invalid_timestamp) headers = { header => signature, timestamp_header => invalid_timestamp } - expect(valid_with(headers:, config: base_config)).to be false + expect(valid_with?(headers:, config: base_config)).to be false end it "returns false for negative timestamp" do @@ -490,7 +490,7 @@ def test_invalid_timestamp(invalid_timestamp, description) def test_iso_timestamp(iso_timestamp, should_be_valid) signature = create_timestamped_signature(iso_timestamp) headers = { header => signature, timestamp_header => iso_timestamp } - expect(valid_with(headers:, config: base_config)).to be should_be_valid + expect(valid_with?(headers:, config: base_config)).to be should_be_valid end it "returns true for valid ISO 8601 UTC timestamp with Z suffix" do @@ -532,7 +532,7 @@ def test_iso_timestamp(iso_timestamp, should_be_valid) # Use uppercase timestamp header name in the request headers headers = { header => signature, timestamp_header.upcase => timestamp } - expect(valid_with(headers:, config: base_config)).to be true + expect(valid_with?(headers:, config: base_config)).to be true end end @@ -543,7 +543,7 @@ def test_iso_timestamp(iso_timestamp, should_be_valid) signature = "sha256=fakesignature" headers = { default_header => signature } - expect(valid_with(headers:)).to be false + expect(valid_with?(headers:)).to be false end it "returns false when Rack::Utils.secure_compare raises an error" do @@ -552,7 +552,7 @@ def test_iso_timestamp(iso_timestamp, should_be_valid) allow(Rack::Utils).to receive(:secure_compare).and_raise(StandardError, "Comparison error") - expect(valid_with(headers:)).to be false + expect(valid_with?(headers:)).to be false end it "returns false when Time.now raises an error" do @@ -575,7 +575,7 @@ def test_iso_timestamp(iso_timestamp, should_be_valid) allow(Time).to receive(:now).and_raise(StandardError, "Time error") - expect(valid_with(headers:, config:)).to be false + expect(valid_with?(headers:, config:)).to be false end end end @@ -836,25 +836,25 @@ def create_tailscale_signature(payload, timestamp, secret) signature_header_value = create_tailscale_signature(payload, timestamp, secret) headers = { "Tailscale-Webhook-Signature" => signature_header_value } - expect(valid_with(payload:, headers:, config:)).to be true + expect(valid_with?(payload:, headers:, config:)).to be true end it "fails with invalid structured signature" do headers = { "Tailscale-Webhook-Signature" => "t=#{timestamp},v1=invalid_signature" } - expect(valid_with(payload:, headers:, config:)).to be false + expect(valid_with?(payload:, headers:, config:)).to be false end it "fails with malformed structured header" do headers = { "Tailscale-Webhook-Signature" => "malformed_header" } - expect(valid_with(payload:, headers:, config:)).to be false + expect(valid_with?(payload:, headers:, config:)).to be false end it "fails when signature key is missing from structured header" do headers = { "Tailscale-Webhook-Signature" => "t=#{timestamp},other=value" } - expect(valid_with(payload:, headers:, config:)).to be false + expect(valid_with?(payload:, headers:, config:)).to be false end it "validates with timestamp tolerance" do @@ -862,7 +862,7 @@ def create_tailscale_signature(payload, timestamp, secret) signature_header_value = create_tailscale_signature(payload, old_timestamp, secret) headers = { "Tailscale-Webhook-Signature" => signature_header_value } - expect(valid_with(payload:, headers:, config:)).to be true + expect(valid_with?(payload:, headers:, config:)).to be true end it "fails when timestamp is too old" do @@ -870,7 +870,7 @@ def create_tailscale_signature(payload, timestamp, secret) signature_header_value = create_tailscale_signature(payload, old_timestamp, secret) headers = { "Tailscale-Webhook-Signature" => signature_header_value } - expect(valid_with(payload:, headers:, config:)).to be false + expect(valid_with?(payload:, headers:, config:)).to be false end it "works without timestamp when not required" do @@ -881,7 +881,7 @@ def create_tailscale_signature(payload, timestamp, secret) signature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new("sha256"), secret, payload) headers = { "Tailscale-Webhook-Signature" => "v1=#{signature}" } - expect(valid_with(payload:, headers:, config: config_with_no_timestamp)).to be true + expect(valid_with?(payload:, headers:, config: config_with_no_timestamp)).to be true end end end diff --git a/spec/unit/lib/hooks/plugins/auth/shared_secret_spec.rb b/spec/unit/lib/hooks/plugins/auth/shared_secret_spec.rb index 64373e13..3d6bb3af 100644 --- a/spec/unit/lib/hooks/plugins/auth/shared_secret_spec.rb +++ b/spec/unit/lib/hooks/plugins/auth/shared_secret_spec.rb @@ -16,7 +16,7 @@ end let(:log) { instance_double(Logger).as_null_object } - def valid_with(args = {}) + def valid_with?(args = {}) args = { config: default_config }.merge(args) described_class.valid?(payload:, **args) end @@ -35,37 +35,37 @@ def valid_with(args = {}) let(:headers) { { default_header => secret } } it "returns true for a valid shared secret" do - expect(valid_with(headers:)).to be true + expect(valid_with?(headers:)).to be true end it "returns false for an invalid shared secret" do bad_headers = { default_header => "wrong-secret" } - expect(valid_with(headers: bad_headers)).to be false + expect(valid_with?(headers: bad_headers)).to be false end it "returns false if secret header is missing" do - expect(valid_with(headers: {})).to be false + expect(valid_with?(headers: {})).to be false end it "returns false if secret is nil or empty" do # Test nil secret via environment variable allow(ENV).to receive(:[]).with("SUPER_WEBHOOK_SECRET").and_return(nil) - expect(valid_with(headers:)).to be false + expect(valid_with?(headers:)).to be false # Test empty secret via environment variable allow(ENV).to receive(:[]).with("SUPER_WEBHOOK_SECRET").and_return("") - expect(valid_with(headers:)).to be false + expect(valid_with?(headers:)).to be false end it "normalizes header names to lowercase" do upcase_headers = { default_header.upcase => secret } - expect(valid_with(headers: upcase_headers)).to be true + expect(valid_with?(headers: upcase_headers)).to be true downcase_headers = { default_header.downcase => secret } - expect(valid_with(headers: downcase_headers)).to be true + expect(valid_with?(headers: downcase_headers)).to be true mixed_case_headers = { "aUtHoRiZaTiOn" => secret } - expect(valid_with(headers: mixed_case_headers)).to be true + expect(valid_with?(headers: mixed_case_headers)).to be true end it "rejects secrets with leading/trailing whitespace for security" do @@ -74,31 +74,31 @@ def valid_with(args = {}) padded_headers = { default_header => padded_secret } # The validator should reject this because the raw value has whitespace - expect(valid_with(headers: padded_headers)).to be false + expect(valid_with?(headers: padded_headers)).to be false # Also test various whitespace patterns - expect(valid_with(headers: { default_header => " #{secret}" })).to be false - expect(valid_with(headers: { default_header => "#{secret} " })).to be false - expect(valid_with(headers: { default_header => "\t#{secret}\t" })).to be false + expect(valid_with?(headers: { default_header => " #{secret}" })).to be false + expect(valid_with?(headers: { default_header => "#{secret} " })).to be false + expect(valid_with?(headers: { default_header => "\t#{secret}\t" })).to be false end it "rejects secrets with control characters" do bad_headers = { default_header => "secret\x00with\x01null" } - expect(valid_with(headers: bad_headers)).to be false + expect(valid_with?(headers: bad_headers)).to be false bad_headers = { default_header => "secret\nwith\nnewline" } - expect(valid_with(headers: bad_headers)).to be false + expect(valid_with?(headers: bad_headers)).to be false bad_headers = { default_header => "secret\twith\ttab" } - expect(valid_with(headers: bad_headers)).to be false + expect(valid_with?(headers: bad_headers)).to be false end it "handles empty header values" do empty_headers = { default_header => "" } - expect(valid_with(headers: empty_headers)).to be false + expect(valid_with?(headers: empty_headers)).to be false nil_headers = { default_header => nil } - expect(valid_with(headers: nil_headers)).to be false + expect(valid_with?(headers: nil_headers)).to be false end it "returns false for secrets exceeding maximum length limit" do @@ -106,7 +106,7 @@ def valid_with(args = {}) oversized_secret = "a" * (1024 + 1) oversized_headers = { default_header => oversized_secret } expect(log).to receive(:warn).with(/exceeds maximum length/) - expect(valid_with(headers: oversized_headers)).to be false + expect(valid_with?(headers: oversized_headers)).to be false end it "returns false for payloads exceeding maximum size limit" do @@ -114,7 +114,7 @@ def valid_with(args = {}) oversized_payload = "a" * (10 * 1024 * 1024 + 1) headers = { default_header => secret } expect(log).to receive(:warn).with(/Payload size exceeds maximum limit/) - expect(valid_with(payload: oversized_payload, headers: headers)).to be false + expect(valid_with?(payload: oversized_payload, headers: headers)).to be false end end @@ -135,7 +135,7 @@ def valid_with(args = {}) it "returns false when secret is in wrong header" do wrong_headers = { "Authorization" => secret } - expect(valid_with(headers: wrong_headers, config: custom_config)).to be false + expect(valid_with?(headers: wrong_headers, config: custom_config)).to be false end it "supports case-insensitive custom header matching" do @@ -153,14 +153,14 @@ def valid_with(args = {}) it "returns false when secret is in non-default header and no config" do custom_headers = { "X-API-Key" => secret } - expect(valid_with(headers: custom_headers, config: no_config)).to be false + expect(valid_with?(headers: custom_headers, config: no_config)).to be false end end context "with invalid configurations" do it "handles nil config gracefully" do headers = { "Authorization" => secret } - expect(valid_with(headers:, config: nil)).to be false + expect(valid_with?(headers:, config: nil)).to be false end it "handles config without auth section" do @@ -174,22 +174,22 @@ def valid_with(args = {}) it "handles different header types" do # String keys (most common) string_headers = { "Authorization" => secret } - expect(valid_with(headers: string_headers)).to be true + expect(valid_with?(headers: string_headers)).to be true # Symbol keys symbol_headers = { Authorization: secret } - expect(valid_with(headers: symbol_headers)).to be true + expect(valid_with?(headers: symbol_headers)).to be true # Mixed keys mixed_headers = { "authorization" => secret } - expect(valid_with(headers: mixed_headers)).to be true + expect(valid_with?(headers: mixed_headers)).to be true end it "handles non-hash headers" do - expect(valid_with(headers: nil)).to be false - expect(valid_with(headers: "not a hash")).to be false - expect(valid_with(headers: [])).to be false - expect(valid_with(headers: 123)).to be false + expect(valid_with?(headers: nil)).to be false + expect(valid_with?(headers: "not a hash")).to be false + expect(valid_with?(headers: [])).to be false + expect(valid_with?(headers: 123)).to be false end it "handles different secret types" do @@ -197,24 +197,24 @@ def valid_with(args = {}) # String secret allow(ENV).to receive(:[]).with("SUPER_WEBHOOK_SECRET").and_return("123") - expect(valid_with(headers:)).to be true + expect(valid_with?(headers:)).to be true # Test that secrets are treated as strings from environment allow(ENV).to receive(:[]).with("SUPER_WEBHOOK_SECRET").and_return("different") - expect(valid_with(headers:)).to be false + expect(valid_with?(headers:)).to be false end it "is case-sensitive for secret values" do headers = { default_header => "MySecret" } allow(ENV).to receive(:[]).with("SUPER_WEBHOOK_SECRET").and_return("MySecret") - expect(valid_with(headers:)).to be true + expect(valid_with?(headers:)).to be true allow(ENV).to receive(:[]).with("SUPER_WEBHOOK_SECRET").and_return("mysecret") - expect(valid_with(headers:)).to be false + expect(valid_with?(headers:)).to be false allow(ENV).to receive(:[]).with("SUPER_WEBHOOK_SECRET").and_return("MYSECRET") - expect(valid_with(headers:)).to be false + expect(valid_with?(headers:)).to be false end it "handles very long secrets" do @@ -222,10 +222,10 @@ def valid_with(args = {}) headers = { default_header => long_secret } allow(ENV).to receive(:[]).with("SUPER_WEBHOOK_SECRET").and_return(long_secret) - expect(valid_with(headers:)).to be true + expect(valid_with?(headers:)).to be true allow(ENV).to receive(:[]).with("SUPER_WEBHOOK_SECRET").and_return(long_secret + "x") - expect(valid_with(headers:)).to be false + expect(valid_with?(headers:)).to be false end it "handles special characters in secrets" do @@ -233,7 +233,7 @@ def valid_with(args = {}) headers = { default_header => special_secret } allow(ENV).to receive(:[]).with("SUPER_WEBHOOK_SECRET").and_return(special_secret) - expect(valid_with(headers:)).to be true + expect(valid_with?(headers:)).to be true end it "handles unicode characters in secrets" do @@ -247,13 +247,13 @@ def valid_with(args = {}) it "uses secure comparison to prevent timing attacks" do # Test that we're using Rack::Utils.secure_compare expect(Rack::Utils).to receive(:secure_compare).with(secret, secret).and_return(true) - expect(valid_with(headers:)).to be true + expect(valid_with?(headers:)).to be true end it "handles exceptions gracefully" do # Mock an exception during validation allow(Rack::Utils).to receive(:secure_compare).and_raise(StandardError.new("test error")) - expect(valid_with(headers:)).to be false + expect(valid_with?(headers:)).to be false end it "rejects headers with suspicious patterns" do @@ -266,7 +266,7 @@ def valid_with(args = {}) suspicious_values.each do |suspicious_value| bad_headers = { default_header => suspicious_value } - expect(valid_with(headers: bad_headers)).to be false + expect(valid_with?(headers: bad_headers)).to be false end end end diff --git a/vendor/cache/ostruct-0.6.1.gem b/vendor/cache/ostruct-0.6.1.gem deleted file mode 100644 index 95788af2..00000000 Binary files a/vendor/cache/ostruct-0.6.1.gem and /dev/null differ diff --git a/vendor/cache/ostruct-0.6.2.gem b/vendor/cache/ostruct-0.6.2.gem new file mode 100644 index 00000000..896eaedc Binary files /dev/null and b/vendor/cache/ostruct-0.6.2.gem differ diff --git a/vendor/cache/rdoc-6.14.0.gem b/vendor/cache/rdoc-6.14.0.gem deleted file mode 100644 index 2dde547a..00000000 Binary files a/vendor/cache/rdoc-6.14.0.gem and /dev/null differ diff --git a/vendor/cache/rdoc-6.14.1.gem b/vendor/cache/rdoc-6.14.1.gem new file mode 100644 index 00000000..5810bfb3 Binary files /dev/null and b/vendor/cache/rdoc-6.14.1.gem differ diff --git a/vendor/cache/rspec-core-3.13.4.gem b/vendor/cache/rspec-core-3.13.4.gem deleted file mode 100644 index 2a2780f9..00000000 Binary files a/vendor/cache/rspec-core-3.13.4.gem and /dev/null differ diff --git a/vendor/cache/rspec-core-3.13.5.gem b/vendor/cache/rspec-core-3.13.5.gem new file mode 100644 index 00000000..64d5cb8f Binary files /dev/null and b/vendor/cache/rspec-core-3.13.5.gem differ diff --git a/vendor/cache/rubocop-1.76.2.gem b/vendor/cache/rubocop-1.76.2.gem deleted file mode 100644 index 1bc1489d..00000000 Binary files a/vendor/cache/rubocop-1.76.2.gem and /dev/null differ diff --git a/vendor/cache/rubocop-1.77.0.gem b/vendor/cache/rubocop-1.77.0.gem new file mode 100644 index 00000000..fa175d30 Binary files /dev/null and b/vendor/cache/rubocop-1.77.0.gem differ