reputable 0.1.1 → 0.1.3
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/.DS_Store +0 -0
- data/Gemfile.lock +76 -0
- data/README.md +81 -26
- data/Rakefile +8 -0
- data/lib/reputable/configuration.rb +32 -5
- data/lib/reputable/middleware.rb +23 -25
- data/lib/reputable/rails.rb +0 -37
- data/lib/reputable/reputation.rb +3 -63
- data/lib/reputable/tracker.rb +1 -9
- data/lib/reputable/version.rb +1 -1
- data/lib/reputable.rb +89 -25
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ca78832e420ad0cacad0c44bbfbf2a682f1b9f702bbaf8aa07a44e4e04d7d53b
|
|
4
|
+
data.tar.gz: 9e15f534984a4e3d67560a98cbede0c596d02d30fb7f7171b380b043b0781384
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 23832e31d409bb3316c33c931e6207f24eb2ea078c2fa79d3d0c4618e4ffd3359f525d0c6b090344bb5d8e79cf9fdc714752ae09180ce973dd353d93015afb6c
|
|
7
|
+
data.tar.gz: 9eb4a989b111a493e7555672057946c2928251a1112e7b75e011bf7183c329ee3b9c6c6cb0e34a163d3803cb5956321b443b25f2d9fcbfca5c90ad16b73cd766
|
data/.DS_Store
ADDED
|
Binary file
|
data/Gemfile.lock
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
PATH
|
|
2
|
+
remote: .
|
|
3
|
+
specs:
|
|
4
|
+
reputable (0.1.3)
|
|
5
|
+
connection_pool (~> 2.2)
|
|
6
|
+
redis (>= 4.0, < 6.0)
|
|
7
|
+
|
|
8
|
+
GEM
|
|
9
|
+
remote: https://rubygems.org/
|
|
10
|
+
specs:
|
|
11
|
+
ast (2.4.3)
|
|
12
|
+
connection_pool (2.5.5)
|
|
13
|
+
diff-lcs (1.6.2)
|
|
14
|
+
json (2.18.0)
|
|
15
|
+
language_server-protocol (3.17.0.5)
|
|
16
|
+
lint_roller (1.1.0)
|
|
17
|
+
parallel (1.27.0)
|
|
18
|
+
parser (3.3.10.0)
|
|
19
|
+
ast (~> 2.4.1)
|
|
20
|
+
racc
|
|
21
|
+
prism (1.7.0)
|
|
22
|
+
racc (1.8.1)
|
|
23
|
+
rack (2.2.21)
|
|
24
|
+
rainbow (3.1.1)
|
|
25
|
+
rake (13.3.1)
|
|
26
|
+
redis (5.4.1)
|
|
27
|
+
redis-client (>= 0.22.0)
|
|
28
|
+
redis-client (0.26.2)
|
|
29
|
+
connection_pool
|
|
30
|
+
regexp_parser (2.11.3)
|
|
31
|
+
rspec (3.13.2)
|
|
32
|
+
rspec-core (~> 3.13.0)
|
|
33
|
+
rspec-expectations (~> 3.13.0)
|
|
34
|
+
rspec-mocks (~> 3.13.0)
|
|
35
|
+
rspec-core (3.13.6)
|
|
36
|
+
rspec-support (~> 3.13.0)
|
|
37
|
+
rspec-expectations (3.13.5)
|
|
38
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
39
|
+
rspec-support (~> 3.13.0)
|
|
40
|
+
rspec-mocks (3.13.7)
|
|
41
|
+
diff-lcs (>= 1.2.0, < 2.0)
|
|
42
|
+
rspec-support (~> 3.13.0)
|
|
43
|
+
rspec-support (3.13.6)
|
|
44
|
+
rubocop (1.82.0)
|
|
45
|
+
json (~> 2.3)
|
|
46
|
+
language_server-protocol (~> 3.17.0.2)
|
|
47
|
+
lint_roller (~> 1.1.0)
|
|
48
|
+
parallel (~> 1.10)
|
|
49
|
+
parser (>= 3.3.0.2)
|
|
50
|
+
rainbow (>= 2.2.2, < 4.0)
|
|
51
|
+
regexp_parser (>= 2.9.3, < 3.0)
|
|
52
|
+
rubocop-ast (>= 1.48.0, < 2.0)
|
|
53
|
+
ruby-progressbar (~> 1.7)
|
|
54
|
+
unicode-display_width (>= 2.4.0, < 4.0)
|
|
55
|
+
rubocop-ast (1.48.0)
|
|
56
|
+
parser (>= 3.3.7.2)
|
|
57
|
+
prism (~> 1.4)
|
|
58
|
+
ruby-progressbar (1.13.0)
|
|
59
|
+
unicode-display_width (3.2.0)
|
|
60
|
+
unicode-emoji (~> 4.1)
|
|
61
|
+
unicode-emoji (4.2.0)
|
|
62
|
+
|
|
63
|
+
PLATFORMS
|
|
64
|
+
arm64-darwin-25
|
|
65
|
+
ruby
|
|
66
|
+
|
|
67
|
+
DEPENDENCIES
|
|
68
|
+
bundler (~> 2.0)
|
|
69
|
+
rack (~> 2.0)
|
|
70
|
+
rake (~> 13.0)
|
|
71
|
+
reputable!
|
|
72
|
+
rspec (~> 3.0)
|
|
73
|
+
rubocop (~> 1.21)
|
|
74
|
+
|
|
75
|
+
BUNDLED WITH
|
|
76
|
+
2.5.9
|
data/README.md
CHANGED
|
@@ -28,9 +28,6 @@ Reputable.configure do |config|
|
|
|
28
28
|
# Required: Redis/Dragonfly URL (TLS supported via rediss://)
|
|
29
29
|
config.redis_url = ENV['REPUTABLE_REDIS_URL']
|
|
30
30
|
|
|
31
|
-
# Required: Your tenant ID
|
|
32
|
-
config.tenant_id = ENV['REPUTABLE_TENANT_ID']
|
|
33
|
-
|
|
34
31
|
# Optional: Enable logging (logs at debug level)
|
|
35
32
|
Reputable.logger = Rails.logger
|
|
36
33
|
end
|
|
@@ -82,7 +79,6 @@ All configuration can be set via environment variables:
|
|
|
82
79
|
```bash
|
|
83
80
|
# Required
|
|
84
81
|
REPUTABLE_REDIS_URL=rediss://user:password@your-dragonfly.example.com:6379
|
|
85
|
-
REPUTABLE_TENANT_ID=your-tenant-id
|
|
86
82
|
|
|
87
83
|
# Optional: Disable entirely (useful for test environments)
|
|
88
84
|
REPUTABLE_ENABLED=false
|
|
@@ -105,9 +101,6 @@ Reputable.configure do |config|
|
|
|
105
101
|
# Redis connection (supports redis:// and rediss:// for TLS)
|
|
106
102
|
config.redis_url = ENV['REPUTABLE_REDIS_URL']
|
|
107
103
|
|
|
108
|
-
# Your tenant identifier
|
|
109
|
-
config.tenant_id = ENV['REPUTABLE_TENANT_ID']
|
|
110
|
-
|
|
111
104
|
# Connection pool settings
|
|
112
105
|
config.pool_size = 5 # Number of Redis connections
|
|
113
106
|
config.pool_timeout = 1.0 # Max wait for connection (seconds)
|
|
@@ -142,6 +135,11 @@ Reputable.configure do |config|
|
|
|
142
135
|
REMOTE_ADDR
|
|
143
136
|
]
|
|
144
137
|
|
|
138
|
+
# Verification Configuration
|
|
139
|
+
# Supports comma-separated list in REPUTABLE_TRUSTED_KEYS or single key in REPUTABLE_TRUSTED_KEY
|
|
140
|
+
config.trusted_keys = ENV['REPUTABLE_TRUSTED_KEYS']&.split(',') || ENV['REPUTABLE_TRUSTED_KEY']
|
|
141
|
+
config.verification_base_url = ENV['REPUTABLE_VERIFY_URL'] # URL of your Reputable API verify endpoint
|
|
142
|
+
|
|
145
143
|
# Error callback (optional)
|
|
146
144
|
config.on_error = ->(error, context) {
|
|
147
145
|
# Report to your error tracking service
|
|
@@ -284,8 +282,6 @@ track_reputable_request(tags: ['custom:tag'])
|
|
|
284
282
|
|
|
285
283
|
# Trust methods (after successful actions)
|
|
286
284
|
trust_current_ip(reason: 'payment_completed', order_id: '123')
|
|
287
|
-
trust_current_user(reason: 'email_verified')
|
|
288
|
-
trust_current_session(reason: 'captcha_passed')
|
|
289
285
|
|
|
290
286
|
# Challenge/Block methods
|
|
291
287
|
challenge_current_ip(reason: 'suspicious_activity')
|
|
@@ -319,11 +315,9 @@ Reputable.track_request(
|
|
|
319
315
|
path: request.path,
|
|
320
316
|
query: request.query_string,
|
|
321
317
|
method: request.request_method,
|
|
322
|
-
session_id: session.id,
|
|
323
|
-
session_present: true,
|
|
324
318
|
user_agent: request.user_agent,
|
|
325
319
|
referer: request.referer,
|
|
326
|
-
tags: ['
|
|
320
|
+
tags: ['view:page:product']
|
|
327
321
|
)
|
|
328
322
|
|
|
329
323
|
# Asynchronous (fire-and-forget, recommended)
|
|
@@ -339,12 +333,6 @@ Reputable.track_request_async(
|
|
|
339
333
|
# Trust IP forever (after payment, verification, etc.)
|
|
340
334
|
Reputable.trust_ip(request.ip, reason: 'payment_completed', order_id: order.id)
|
|
341
335
|
|
|
342
|
-
# Trust a user
|
|
343
|
-
Reputable.trust_user(current_user.id, reason: 'email_verified')
|
|
344
|
-
|
|
345
|
-
# Trust a session (default: 24 hour TTL)
|
|
346
|
-
Reputable.trust_session(session.id, reason: 'captcha_passed')
|
|
347
|
-
|
|
348
336
|
# Challenge (require CAPTCHA, etc.)
|
|
349
337
|
Reputable.challenge_ip(request.ip, reason: 'unusual_activity')
|
|
350
338
|
|
|
@@ -372,12 +360,80 @@ Reputable.lookup_reputation(:ip, request.ip)
|
|
|
372
360
|
# => { status: "trusted_verified", reason: "payment_completed",
|
|
373
361
|
# source: "app_server", updated_at: 1703123456789,
|
|
374
362
|
# expires_at: 0, metadata: { order_id: "123" } }
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
---
|
|
366
|
+
|
|
367
|
+
## User Verification & Trust Flow
|
|
368
|
+
|
|
369
|
+
When you identify a suspicious user (e.g., high risk score or specific tag), you can redirect them to the Reputable verification page. This page performs advanced browser checks and challenges (CAPTCHA) if necessary.
|
|
370
|
+
|
|
371
|
+
### 1. Generating a Signed Redirect URL
|
|
372
|
+
|
|
373
|
+
To effectively hand off verification handling to Reputable, generate a signed verification URL:
|
|
374
|
+
|
|
375
|
+
```ruby
|
|
376
|
+
# In your controller
|
|
377
|
+
if suspicious_activity_detected?
|
|
378
|
+
redirect_url = Reputable.verification_url(
|
|
379
|
+
return_url: request.original_url, # Where to send them back after verification
|
|
380
|
+
failure_url: root_url, # Optional: where to send if they fail/garbage token
|
|
381
|
+
session_id: session.id # Optional: link specific session
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
redirect_to redirect_url
|
|
385
|
+
end
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
### 2. Handling the Return Redirect
|
|
389
|
+
|
|
390
|
+
When the user passes verification (or is determined to be already trusted/clean), they are immediately redirected back to your `return_url` with signed parameters.
|
|
391
|
+
|
|
392
|
+
**Middleware (Automatic Handling):**
|
|
393
|
+
If you are using `Reputable::Middleware` (recommended), this is handled automatically. The middleware detects the return parameters, verifies the signature, and sets `env['reputable.verified'] = true`.
|
|
394
|
+
|
|
395
|
+
```ruby
|
|
396
|
+
# In your controller
|
|
397
|
+
if request.env['reputable.verified']
|
|
398
|
+
# User just passed verification!
|
|
399
|
+
# You might want to graduate them to trusted status locally
|
|
400
|
+
trust_current_ip(reason: 'interactive_verification')
|
|
401
|
+
end
|
|
402
|
+
```
|
|
375
403
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
404
|
+
**Manual Verification:**
|
|
405
|
+
If you need to verify manually (e.g., custom controller logic):
|
|
406
|
+
|
|
407
|
+
```ruby
|
|
408
|
+
# In your controller action (the return_url target)
|
|
409
|
+
if params[:reputable_signature]
|
|
410
|
+
if Reputable.verify_redirect_return(params)
|
|
411
|
+
# Signature is VALID. Meaning Reputable actually sent this user back.
|
|
412
|
+
|
|
413
|
+
# Inspect outcomes
|
|
414
|
+
status = params[:reputable_status] # 'pass', 'fail'
|
|
415
|
+
outcome = params[:reputable_outcome] # 'trusted_verified', 'trusted_behavior', 'unknown'
|
|
416
|
+
country = params[:reputable_country] # 'US', 'DE', etc.
|
|
417
|
+
|
|
418
|
+
if status == 'pass'
|
|
419
|
+
# Grant access
|
|
420
|
+
end
|
|
421
|
+
else
|
|
422
|
+
# Invalid signature - potential tampering attempt!
|
|
423
|
+
render plain: "Verification failed", status: 403
|
|
424
|
+
end
|
|
425
|
+
end
|
|
379
426
|
```
|
|
380
427
|
|
|
428
|
+
**Return Parameters:**
|
|
429
|
+
The return URL will contain:
|
|
430
|
+
- `reputable_status`: `pass` or `fail`
|
|
431
|
+
- `reputable_session_id`: The session ID you provided
|
|
432
|
+
- `reputable_outcome`: The specific reputation outcome (e.g., `trusted_verified`)
|
|
433
|
+
- `reputable_ignore_analytics`: 'true'/'false' flag
|
|
434
|
+
- `reputable_country`: ISO country code
|
|
435
|
+
- `reputable_signature`: HMAC-SHA256 signature of the above
|
|
436
|
+
|
|
381
437
|
---
|
|
382
438
|
|
|
383
439
|
## Resilience & Failsafe Features
|
|
@@ -438,10 +494,10 @@ Use tags to classify requests for behavioral analysis:
|
|
|
438
494
|
|
|
439
495
|
```ruby
|
|
440
496
|
# Page context
|
|
441
|
-
tags: ['
|
|
442
|
-
tags: ['
|
|
443
|
-
tags: ['
|
|
444
|
-
tags: ['
|
|
497
|
+
tags: ['view:page:product']
|
|
498
|
+
tags: ['view:page:checkout']
|
|
499
|
+
tags: ['view:page:cart']
|
|
500
|
+
tags: ['view:page:login']
|
|
445
501
|
|
|
446
502
|
# Traffic source
|
|
447
503
|
tags: ['trust:channel:email']
|
|
@@ -493,7 +549,6 @@ expect(Reputable.lookup_ip('1.2.3.4')).to be_nil
|
|
|
493
549
|
### What's Available from Rails
|
|
494
550
|
|
|
495
551
|
- IP reputation and history
|
|
496
|
-
- Session tracking
|
|
497
552
|
- Request classification
|
|
498
553
|
- UA churn detection
|
|
499
554
|
- Cross-request pattern analysis
|
data/Rakefile
ADDED
|
@@ -9,12 +9,12 @@ module Reputable
|
|
|
9
9
|
# Supports TLS connections with automatic SSL error handling.
|
|
10
10
|
# All SSL/connection errors are caught and logged, never breaking your app.
|
|
11
11
|
class Configuration
|
|
12
|
-
attr_accessor :redis_url, :redis_options, :
|
|
12
|
+
attr_accessor :redis_url, :redis_options, :buffer_prefix,
|
|
13
13
|
:request_buffer_key, :reputation_buffer_key,
|
|
14
14
|
:default_ttls, :pool_size, :pool_timeout,
|
|
15
15
|
:connect_timeout, :read_timeout, :write_timeout,
|
|
16
16
|
:ssl_params, :trusted_proxies, :ip_header_priority,
|
|
17
|
-
:on_error
|
|
17
|
+
:on_error, :trusted_keys, :verification_base_url
|
|
18
18
|
|
|
19
19
|
# Default TTLs in seconds (0 = forever)
|
|
20
20
|
DEFAULT_TTLS = {
|
|
@@ -52,7 +52,6 @@ module Reputable
|
|
|
52
52
|
def initialize
|
|
53
53
|
@redis_url = ENV.fetch("REPUTABLE_REDIS_URL", "redis://127.0.0.1:6379")
|
|
54
54
|
@redis_options = {}
|
|
55
|
-
@tenant_id = ENV.fetch("REPUTABLE_TENANT_ID", "default")
|
|
56
55
|
@buffer_prefix = "reputable:buffer"
|
|
57
56
|
@pool_size = Integer(ENV.fetch("REPUTABLE_POOL_SIZE", "5"))
|
|
58
57
|
@pool_timeout = Float(ENV.fetch("REPUTABLE_POOL_TIMEOUT", "1.0"))
|
|
@@ -64,6 +63,34 @@ module Reputable
|
|
|
64
63
|
@trusted_proxies = nil # Additional trusted proxy IPs/ranges
|
|
65
64
|
@ip_header_priority = DEFAULT_IP_HEADERS.dup
|
|
66
65
|
@on_error = nil # Optional error callback: ->(error, context) { ... }
|
|
66
|
+
|
|
67
|
+
# Support multiple trusted keys (comma-separated), fallback to old secret key env var
|
|
68
|
+
@trusted_keys = []
|
|
69
|
+
if ENV["REPUTABLE_TRUSTED_KEYS"]
|
|
70
|
+
@trusted_keys = ENV["REPUTABLE_TRUSTED_KEYS"].split(",").map(&:strip)
|
|
71
|
+
elsif ENV["REPUTABLE_TRUSTED_KEY"]
|
|
72
|
+
@trusted_keys = [ENV["REPUTABLE_TRUSTED_KEY"]]
|
|
73
|
+
elsif ENV["REPUTABLE_SECRET_KEY"]
|
|
74
|
+
@trusted_keys = [ENV["REPUTABLE_SECRET_KEY"]]
|
|
75
|
+
end
|
|
76
|
+
@verification_base_url = ENV.fetch("REPUTABLE_VERIFY_URL", "https://api.reputable.click/_reputable/verify")
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Alias for backward compatibility
|
|
80
|
+
def secret_key
|
|
81
|
+
@trusted_keys.first
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def secret_key=(key)
|
|
85
|
+
@trusted_keys = [key]
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def trusted_keys=(keys)
|
|
89
|
+
@trusted_keys = Array(keys)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def trusted_keys
|
|
93
|
+
@trusted_keys
|
|
67
94
|
end
|
|
68
95
|
|
|
69
96
|
# Check if Reputable is enabled (can be disabled via ENV)
|
|
@@ -75,11 +102,11 @@ module Reputable
|
|
|
75
102
|
end
|
|
76
103
|
|
|
77
104
|
def request_buffer_key
|
|
78
|
-
@request_buffer_key || "#{buffer_prefix}:requests
|
|
105
|
+
@request_buffer_key || "#{buffer_prefix}:requests"
|
|
79
106
|
end
|
|
80
107
|
|
|
81
108
|
def reputation_buffer_key
|
|
82
|
-
@reputation_buffer_key || "#{buffer_prefix}:reputation
|
|
109
|
+
@reputation_buffer_key || "#{buffer_prefix}:reputation"
|
|
83
110
|
end
|
|
84
111
|
|
|
85
112
|
def default_ttl_for(status)
|
data/lib/reputable/middleware.rb
CHANGED
|
@@ -45,6 +45,9 @@ module Reputable
|
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
def call(env)
|
|
48
|
+
# Check for verification return parameters and verify signature if present
|
|
49
|
+
handle_verification_return(env)
|
|
50
|
+
|
|
48
51
|
# ALWAYS process the request first - tracking must never block
|
|
49
52
|
status, headers, response = @app.call(env)
|
|
50
53
|
|
|
@@ -70,6 +73,26 @@ module Reputable
|
|
|
70
73
|
Reputable.logger&.debug("Reputable middleware: #{e.class} - #{e.message}")
|
|
71
74
|
end
|
|
72
75
|
|
|
76
|
+
def handle_verification_return(env)
|
|
77
|
+
request = Rack::Request.new(env)
|
|
78
|
+
# Quick check to avoid overhead
|
|
79
|
+
return unless request.query_string.include?("reputable_status")
|
|
80
|
+
|
|
81
|
+
return unless request.params["reputable_status"] == "pass"
|
|
82
|
+
|
|
83
|
+
if Reputable.verify_redirect_return(request.params)
|
|
84
|
+
env["reputable.verified"] = true
|
|
85
|
+
|
|
86
|
+
# Store in session if available
|
|
87
|
+
if env["rack.session"]
|
|
88
|
+
env["rack.session"]["reputable_verified"] = true
|
|
89
|
+
env["rack.session"]["reputable_verified_at"] = Time.now.to_i
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
rescue StandardError => e
|
|
93
|
+
Reputable.logger&.debug("Reputable verification error: #{e.message}")
|
|
94
|
+
end
|
|
95
|
+
|
|
73
96
|
def skip_request?(env)
|
|
74
97
|
return true if @skip_if&.call(env)
|
|
75
98
|
|
|
@@ -109,8 +132,6 @@ module Reputable
|
|
|
109
132
|
path: request.path,
|
|
110
133
|
query: request.query_string.empty? ? nil : request.query_string,
|
|
111
134
|
method: request.request_method,
|
|
112
|
-
session_id: extract_session_id(env),
|
|
113
|
-
session_present: session_present?(env),
|
|
114
135
|
user_agent: env["HTTP_USER_AGENT"],
|
|
115
136
|
referer: env["HTTP_REFERER"],
|
|
116
137
|
tags: build_tags(env)
|
|
@@ -190,29 +211,6 @@ module Reputable
|
|
|
190
211
|
nil
|
|
191
212
|
end
|
|
192
213
|
|
|
193
|
-
def extract_session_id(env)
|
|
194
|
-
# Try rack.session.id first
|
|
195
|
-
return env["rack.session.id"] if env["rack.session.id"]
|
|
196
|
-
|
|
197
|
-
# Try to get from rack.session
|
|
198
|
-
session = env["rack.session"]
|
|
199
|
-
return session.id.to_s if session.respond_to?(:id)
|
|
200
|
-
|
|
201
|
-
nil
|
|
202
|
-
rescue StandardError
|
|
203
|
-
nil
|
|
204
|
-
end
|
|
205
|
-
|
|
206
|
-
def session_present?(env)
|
|
207
|
-
return true if env["rack.session.id"]
|
|
208
|
-
return true if env["rack.session"]&.any?
|
|
209
|
-
return true if env["HTTP_COOKIE"]&.include?("_session")
|
|
210
|
-
|
|
211
|
-
false
|
|
212
|
-
rescue StandardError
|
|
213
|
-
false
|
|
214
|
-
end
|
|
215
|
-
|
|
216
214
|
def build_tags(env)
|
|
217
215
|
tags = []
|
|
218
216
|
|
data/lib/reputable/rails.rb
CHANGED
|
@@ -14,8 +14,6 @@ module Reputable
|
|
|
14
14
|
path: request.path,
|
|
15
15
|
query: request.query_string,
|
|
16
16
|
method: request.method,
|
|
17
|
-
session_id: session.id.to_s,
|
|
18
|
-
session_present: session.any?,
|
|
19
17
|
user_agent: request.user_agent,
|
|
20
18
|
referer: request.referer,
|
|
21
19
|
tags: tags,
|
|
@@ -32,26 +30,6 @@ module Reputable
|
|
|
32
30
|
)
|
|
33
31
|
end
|
|
34
32
|
|
|
35
|
-
# Trust the current user
|
|
36
|
-
def trust_current_user(reason:, **metadata)
|
|
37
|
-
return false unless respond_to?(:current_user) && current_user
|
|
38
|
-
|
|
39
|
-
Reputable::Reputation.trust_user(
|
|
40
|
-
current_user.id,
|
|
41
|
-
reason: reason,
|
|
42
|
-
**metadata
|
|
43
|
-
)
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
# Trust the current session
|
|
47
|
-
def trust_current_session(reason:, **metadata)
|
|
48
|
-
Reputable::Reputation.trust_session(
|
|
49
|
-
session.id.to_s,
|
|
50
|
-
reason: reason,
|
|
51
|
-
**metadata
|
|
52
|
-
)
|
|
53
|
-
end
|
|
54
|
-
|
|
55
33
|
# Challenge the current IP
|
|
56
34
|
def challenge_current_ip(reason:, **metadata)
|
|
57
35
|
Reputable::Reputation.challenge_ip(
|
|
@@ -103,20 +81,6 @@ module Reputable
|
|
|
103
81
|
def current_ip_status
|
|
104
82
|
Reputable::Reputation.lookup_ip(request.remote_ip)
|
|
105
83
|
end
|
|
106
|
-
|
|
107
|
-
# Check if current user is trusted
|
|
108
|
-
# @return [Boolean]
|
|
109
|
-
def current_user_trusted?
|
|
110
|
-
return false unless respond_to?(:current_user) && current_user
|
|
111
|
-
|
|
112
|
-
Reputable::Reputation.trusted_user?(current_user.id)
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
# Check if current session is trusted
|
|
116
|
-
# @return [Boolean]
|
|
117
|
-
def current_session_trusted?
|
|
118
|
-
Reputable::Reputation.trusted_session?(session.id.to_s)
|
|
119
|
-
end
|
|
120
84
|
end
|
|
121
85
|
|
|
122
86
|
# Railtie for automatic Rails integration (only defined when Rails is present)
|
|
@@ -130,7 +94,6 @@ module Reputable
|
|
|
130
94
|
creds = ::Rails.application.credentials.reputable rescue nil
|
|
131
95
|
if creds
|
|
132
96
|
config.redis_url = creds[:redis_url] if creds[:redis_url]
|
|
133
|
-
config.tenant_id = creds[:tenant_id] if creds[:tenant_id]
|
|
134
97
|
end
|
|
135
98
|
end
|
|
136
99
|
end
|
data/lib/reputable/reputation.rb
CHANGED
|
@@ -19,12 +19,12 @@ module Reputable
|
|
|
19
19
|
untrusted_ignore
|
|
20
20
|
].freeze
|
|
21
21
|
|
|
22
|
-
VALID_ENTITY_TYPES = %i[ip asn ja4
|
|
22
|
+
VALID_ENTITY_TYPES = %i[ip asn ja4].freeze
|
|
23
23
|
|
|
24
24
|
class << self
|
|
25
25
|
# Apply a reputation status to an entity
|
|
26
26
|
#
|
|
27
|
-
# @param entity_type [Symbol] Type of entity (:ip, :asn, :ja4
|
|
27
|
+
# @param entity_type [Symbol] Type of entity (:ip, :asn, :ja4)
|
|
28
28
|
# @param entity_id [String] The entity identifier (IP address, ASN, etc.)
|
|
29
29
|
# @param status [Symbol] Reputation status to apply
|
|
30
30
|
# @param options [Hash] Additional options
|
|
@@ -110,30 +110,6 @@ module Reputable
|
|
|
110
110
|
)
|
|
111
111
|
end
|
|
112
112
|
|
|
113
|
-
# Trust a user (by internal user ID)
|
|
114
|
-
def trust_user(user_id, reason: "verified_user", **metadata)
|
|
115
|
-
apply(
|
|
116
|
-
entity_type: :user,
|
|
117
|
-
entity_id: user_id.to_s,
|
|
118
|
-
status: :trusted_verified,
|
|
119
|
-
reason: reason,
|
|
120
|
-
ttl: 0,
|
|
121
|
-
metadata: metadata
|
|
122
|
-
)
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
# Trust a session
|
|
126
|
-
def trust_session(session_id, reason: "verified_session", ttl: nil, **metadata)
|
|
127
|
-
apply(
|
|
128
|
-
entity_type: :session,
|
|
129
|
-
entity_id: session_id,
|
|
130
|
-
status: :trusted_verified,
|
|
131
|
-
reason: reason,
|
|
132
|
-
ttl: ttl || (24 * 3600), # Default 24 hours for sessions
|
|
133
|
-
metadata: metadata
|
|
134
|
-
)
|
|
135
|
-
end
|
|
136
|
-
|
|
137
113
|
# ========================================================================
|
|
138
114
|
# LOOKUP METHODS (O(1) Redis hash lookups)
|
|
139
115
|
# ========================================================================
|
|
@@ -141,7 +117,7 @@ module Reputable
|
|
|
141
117
|
# Lookup the current reputation status for an entity
|
|
142
118
|
# This is an O(1) Redis HGETALL operation
|
|
143
119
|
#
|
|
144
|
-
# @param entity_type [Symbol] Type of entity (:ip, :asn, :ja4
|
|
120
|
+
# @param entity_type [Symbol] Type of entity (:ip, :asn, :ja4)
|
|
145
121
|
# @param entity_id [String] The entity identifier
|
|
146
122
|
# @return [Hash, nil] Reputation data or nil if not found/expired/error
|
|
147
123
|
#
|
|
@@ -227,42 +203,6 @@ module Reputable
|
|
|
227
203
|
lookup_ip(ip) == "untrusted_challenge"
|
|
228
204
|
end
|
|
229
205
|
|
|
230
|
-
# Lookup user reputation
|
|
231
|
-
#
|
|
232
|
-
# @param user_id [String, Integer] User ID
|
|
233
|
-
# @return [String, nil] Status string or nil
|
|
234
|
-
def lookup_user(user_id)
|
|
235
|
-
result = lookup(:user, user_id.to_s)
|
|
236
|
-
result&.dig(:status)
|
|
237
|
-
end
|
|
238
|
-
|
|
239
|
-
# Check if a user is trusted
|
|
240
|
-
#
|
|
241
|
-
# @param user_id [String, Integer] User ID
|
|
242
|
-
# @return [Boolean]
|
|
243
|
-
def trusted_user?(user_id)
|
|
244
|
-
status = lookup_user(user_id)
|
|
245
|
-
status&.start_with?("trusted")
|
|
246
|
-
end
|
|
247
|
-
|
|
248
|
-
# Lookup session reputation
|
|
249
|
-
#
|
|
250
|
-
# @param session_id [String] Session ID
|
|
251
|
-
# @return [String, nil] Status string or nil
|
|
252
|
-
def lookup_session(session_id)
|
|
253
|
-
result = lookup(:session, session_id)
|
|
254
|
-
result&.dig(:status)
|
|
255
|
-
end
|
|
256
|
-
|
|
257
|
-
# Check if a session is trusted
|
|
258
|
-
#
|
|
259
|
-
# @param session_id [String] Session ID
|
|
260
|
-
# @return [Boolean]
|
|
261
|
-
def trusted_session?(session_id)
|
|
262
|
-
status = lookup_session(session_id)
|
|
263
|
-
status&.start_with?("trusted")
|
|
264
|
-
end
|
|
265
|
-
|
|
266
206
|
private
|
|
267
207
|
|
|
268
208
|
def valid_entity_type?(entity_type)
|
data/lib/reputable/tracker.rb
CHANGED
|
@@ -18,9 +18,6 @@ module Reputable
|
|
|
18
18
|
# @param options [Hash] Additional options
|
|
19
19
|
# @option options [String] :query Query string
|
|
20
20
|
# @option options [String] :method HTTP method (GET, POST, etc.)
|
|
21
|
-
# @option options [String] :session_id Session identifier
|
|
22
|
-
# @option options [Boolean] :session_present Whether a session cookie exists
|
|
23
|
-
# @option options [String] :user_id Internal user ID (for logged-in users)
|
|
24
21
|
# @option options [String] :fingerprint Browser fingerprint hash
|
|
25
22
|
# @option options [String] :user_agent User-Agent header
|
|
26
23
|
# @option options [String] :referer Referer header
|
|
@@ -41,11 +38,9 @@ module Reputable
|
|
|
41
38
|
# path: request.path,
|
|
42
39
|
# query: request.query_string,
|
|
43
40
|
# method: request.request_method,
|
|
44
|
-
# session_id: session.id,
|
|
45
|
-
# session_present: true,
|
|
46
41
|
# user_agent: request.user_agent,
|
|
47
42
|
# referer: request.referer,
|
|
48
|
-
# tags: ["
|
|
43
|
+
# tags: ["view:page:product", "trust:channel:organic"]
|
|
49
44
|
# )
|
|
50
45
|
def track_request(ip:, path:, **options)
|
|
51
46
|
return false unless Reputable.enabled?
|
|
@@ -80,9 +75,6 @@ module Reputable
|
|
|
80
75
|
path: path,
|
|
81
76
|
query: options[:query],
|
|
82
77
|
method: options[:method] || "GET",
|
|
83
|
-
session_id: options[:session_id],
|
|
84
|
-
session_present: options[:session_present],
|
|
85
|
-
user_id: options[:user_id],
|
|
86
78
|
fingerprint: options[:fingerprint],
|
|
87
79
|
user_agent: options[:user_agent],
|
|
88
80
|
referer: options[:referer],
|
data/lib/reputable/version.rb
CHANGED
data/lib/reputable.rb
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "reputable/version"
|
|
4
|
+
require "json"
|
|
5
|
+
require "base64"
|
|
4
6
|
require_relative "reputable/configuration"
|
|
5
7
|
require_relative "reputable/connection"
|
|
6
8
|
require_relative "reputable/tracker"
|
|
@@ -23,14 +25,12 @@ end
|
|
|
23
25
|
# @example Basic setup
|
|
24
26
|
# Reputable.configure do |config|
|
|
25
27
|
# config.redis_url = "rediss://user:pass@dragonfly.example.com:6379"
|
|
26
|
-
# config.tenant_id = "my-tenant"
|
|
27
28
|
# end
|
|
28
29
|
#
|
|
29
30
|
# @example Track a request
|
|
30
31
|
# Reputable.track_request(
|
|
31
32
|
# ip: "203.0.113.45",
|
|
32
|
-
# path: "/products/123"
|
|
33
|
-
# session_present: true
|
|
33
|
+
# path: "/products/123"
|
|
34
34
|
# )
|
|
35
35
|
#
|
|
36
36
|
# @example Apply reputation after payment
|
|
@@ -104,14 +104,6 @@ module Reputable
|
|
|
104
104
|
Reputation.ignore_ip(ip, reason: reason, **metadata)
|
|
105
105
|
end
|
|
106
106
|
|
|
107
|
-
def trust_user(user_id, reason: "verified_user", **metadata)
|
|
108
|
-
Reputation.trust_user(user_id, reason: reason, **metadata)
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
def trust_session(session_id, reason: "verified_session", **metadata)
|
|
112
|
-
Reputation.trust_session(session_id, reason: reason, **metadata)
|
|
113
|
-
end
|
|
114
|
-
|
|
115
107
|
# Delegate lookup methods to Reputation module (O(1) Redis lookups)
|
|
116
108
|
def lookup_reputation(entity_type, entity_id)
|
|
117
109
|
Reputation.lookup(entity_type, entity_id)
|
|
@@ -133,20 +125,92 @@ module Reputable
|
|
|
133
125
|
Reputation.challenged_ip?(ip)
|
|
134
126
|
end
|
|
135
127
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
128
|
+
# Generate a signed verification URL
|
|
129
|
+
def verification_url(return_url:, failure_url: nil, session_id: nil)
|
|
130
|
+
keys = configuration.trusted_keys
|
|
131
|
+
if keys.nil? || keys.empty?
|
|
132
|
+
logger&.warn "Reputable: Missing trusted_keys, cannot generate verification URL"
|
|
133
|
+
return return_url
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Use the first key for signing new requests
|
|
137
|
+
secret = keys.first
|
|
138
|
+
|
|
139
|
+
base_url = configuration.verification_base_url
|
|
140
|
+
|
|
141
|
+
# JWT Header
|
|
142
|
+
header = { alg: "HS256", typ: "JWT" }
|
|
143
|
+
encoded_header = base64url_encode(JSON.generate(header))
|
|
144
|
+
|
|
145
|
+
# JWT Payload
|
|
146
|
+
payload = {
|
|
147
|
+
returnUrl: return_url,
|
|
148
|
+
failureUrl: failure_url,
|
|
149
|
+
sessionId: session_id,
|
|
150
|
+
iat: Time.now.to_i
|
|
151
|
+
}
|
|
152
|
+
encoded_payload = base64url_encode(JSON.generate(payload))
|
|
153
|
+
|
|
154
|
+
# Signature
|
|
155
|
+
data = "#{encoded_header}.#{encoded_payload}"
|
|
156
|
+
signature = OpenSSL::HMAC.digest("SHA256", secret, data)
|
|
157
|
+
encoded_signature = base64url_encode(signature)
|
|
158
|
+
|
|
159
|
+
token = "#{data}.#{encoded_signature}"
|
|
160
|
+
|
|
161
|
+
"#{base_url}?token=#{token}"
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
# Verify the signature of a redirect return
|
|
165
|
+
# @param params [Hash] Request query parameters
|
|
166
|
+
# @return [Boolean] true if valid logic and passed signature check
|
|
167
|
+
def verify_redirect_return(params)
|
|
168
|
+
status = params["reputable_status"]
|
|
169
|
+
session_id = params["reputable_session_id"]
|
|
170
|
+
signature = params["reputable_signature"]
|
|
171
|
+
outcome = params["reputable_outcome"]
|
|
172
|
+
ignore_analytics = params["reputable_ignore_analytics"]
|
|
173
|
+
country = params["reputable_country"] || ""
|
|
174
|
+
|
|
175
|
+
return false unless status && session_id && signature
|
|
176
|
+
|
|
177
|
+
keys = configuration.trusted_keys
|
|
178
|
+
if keys.nil? || keys.empty?
|
|
179
|
+
logger&.warn "Reputable: Missing trusted_keys, cannot verify redirect"
|
|
180
|
+
return false
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Reconstruct data string: status:sessionId:outcome:ignoreAnalytics:country
|
|
184
|
+
# Note: optional params default to empty strings if missing in reconstruction logic on server
|
|
185
|
+
data_parts = [
|
|
186
|
+
status,
|
|
187
|
+
session_id,
|
|
188
|
+
outcome || "",
|
|
189
|
+
ignore_analytics.nil? ? "" : ignore_analytics,
|
|
190
|
+
country
|
|
191
|
+
]
|
|
192
|
+
|
|
193
|
+
data = data_parts.join(":")
|
|
194
|
+
|
|
195
|
+
# Iterate through all trusted keys to find a match
|
|
196
|
+
keys.any? do |key|
|
|
197
|
+
expected_signature = OpenSSL::HMAC.hexdigest("SHA256", key, data)
|
|
198
|
+
secure_compare(expected_signature, signature)
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
private
|
|
203
|
+
|
|
204
|
+
def base64url_encode(str)
|
|
205
|
+
Base64.urlsafe_encode64(str).gsub("=", "")
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def secure_compare(a, b)
|
|
209
|
+
return false unless a.bytesize == b.bytesize
|
|
210
|
+
l = a.unpack "C#{a.bytesize}"
|
|
211
|
+
res = 0
|
|
212
|
+
b.each_byte { |byte| res |= byte ^ l.shift }
|
|
213
|
+
res == 0
|
|
150
214
|
end
|
|
151
215
|
end
|
|
152
216
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: reputable
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Reputable
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2025-12-
|
|
11
|
+
date: 2025-12-26 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: redis
|
|
@@ -122,8 +122,11 @@ executables: []
|
|
|
122
122
|
extensions: []
|
|
123
123
|
extra_rdoc_files: []
|
|
124
124
|
files:
|
|
125
|
+
- ".DS_Store"
|
|
125
126
|
- Gemfile
|
|
127
|
+
- Gemfile.lock
|
|
126
128
|
- README.md
|
|
129
|
+
- Rakefile
|
|
127
130
|
- lib/reputable.rb
|
|
128
131
|
- lib/reputable/configuration.rb
|
|
129
132
|
- lib/reputable/connection.rb
|