reputable 0.1.17 → 0.1.18
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/Gemfile.lock +1 -1
- data/README.md +65 -0
- data/lib/reputable/configuration.rb +21 -1
- data/lib/reputable/middleware.rb +34 -0
- data/lib/reputable/reputation.rb +105 -0
- data/lib/reputable/version.rb +1 -1
- data/lib/reputable.rb +33 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 40be87edb5494144f499f263572e00d98331cd07913239fca32b5ce71450df26
|
|
4
|
+
data.tar.gz: 63cfe644b94396a6d72b05a75a45076e8bc41196f6c4970307992e6260d6f86f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ed8aee9a76c98611718621fde0c1dada4aff7449092c9fc6cd1990d545b91ee1641c169d841eea12b2b0eeb56edc399e2f8db612041565a2d90dbf76bad34d92
|
|
7
|
+
data.tar.gz: 5b2eeb4fb8ddbf12beeb989b0a33aa5f42352c6a131ca1211ad5475b19978ba8244dbf52504555256aa0d79eb58935ab23be05cd503405d11d35b835bca506c0
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
|
@@ -305,6 +305,44 @@ Notes:
|
|
|
305
305
|
- Use `blocked_page_path` only for local blocked pages (or to build a custom `failure_url`).
|
|
306
306
|
- Override `challenge_redirect_status` (default `302`) or `verification_force_challenge` if needed.
|
|
307
307
|
|
|
308
|
+
### ASN Fallback
|
|
309
|
+
|
|
310
|
+
When an IP has no reputation, the middleware can fall back to checking ASN reputation. This is useful for blocking/challenging entire ASNs (e.g., datacenter ASNs known for abuse).
|
|
311
|
+
|
|
312
|
+
**Enable via environment variable:**
|
|
313
|
+
```bash
|
|
314
|
+
REPUTABLE_ASN_FALLBACK=true
|
|
315
|
+
REPUTABLE_ASN_HEADER=HTTP_X_ASN # Optional, defaults to HTTP_X_ASN
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
**Enable via configuration:**
|
|
319
|
+
```ruby
|
|
320
|
+
Reputable.configure do |config|
|
|
321
|
+
config.asn_fallback = true
|
|
322
|
+
config.asn_header = "HTTP_X_ASN" # Or HTTP_CF_ASN for Cloudflare, etc.
|
|
323
|
+
end
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
**Enable via middleware option:**
|
|
327
|
+
```ruby
|
|
328
|
+
config.middleware.use Reputable::Middleware,
|
|
329
|
+
reputation_gate: true,
|
|
330
|
+
asn_fallback: true
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
**How it works:**
|
|
334
|
+
1. Middleware looks up IP reputation first
|
|
335
|
+
2. If IP has no reputation and ASN fallback is enabled, it extracts ASN from the configured header
|
|
336
|
+
3. If ASN has a reputation (blocked, challenged), that decision is applied
|
|
337
|
+
4. The `env['reputable.reputation_source']` is set to `'asn'` when using ASN-based decision
|
|
338
|
+
|
|
339
|
+
**Providing ASN from your app:**
|
|
340
|
+
If your app has its own GeoIP lookup, set the ASN directly:
|
|
341
|
+
```ruby
|
|
342
|
+
# In a before_action or middleware
|
|
343
|
+
request.env['reputable.asn'] = lookup_asn_for_ip(request.remote_ip)
|
|
344
|
+
```
|
|
345
|
+
|
|
308
346
|
### Server/JS Request Reconciliation
|
|
309
347
|
|
|
310
348
|
When using both server-side tracking (Rack middleware) and client-side JavaScript tracking, requests can be double-counted. The reconciliation system prevents this by correlating requests using a unique `request_id`.
|
|
@@ -501,6 +539,33 @@ Reputable.lookup_reputation(:ip, request.ip)
|
|
|
501
539
|
# expires_at: 0, metadata: { order_id: "123" } }
|
|
502
540
|
```
|
|
503
541
|
|
|
542
|
+
### ASN Reputation
|
|
543
|
+
|
|
544
|
+
Apply and lookup reputations for entire ASNs (Autonomous System Numbers). Useful for blocking datacenter traffic or known-bad networks.
|
|
545
|
+
|
|
546
|
+
```ruby
|
|
547
|
+
# Apply reputation to an ASN
|
|
548
|
+
Reputable.block_asn("15169", reason: "datacenter_abuse")
|
|
549
|
+
Reputable.challenge_asn("7922", reason: "suspicious_traffic")
|
|
550
|
+
Reputable.trust_asn("16509", reason: "known_partner")
|
|
551
|
+
Reputable.ignore_asn("32934", reason: "internal_monitoring")
|
|
552
|
+
|
|
553
|
+
# Quick boolean checks
|
|
554
|
+
Reputable.blocked_asn?("15169") # => true/false
|
|
555
|
+
Reputable.challenged_asn?("7922") # => true/false
|
|
556
|
+
Reputable.trusted_asn?("16509") # => true/false
|
|
557
|
+
|
|
558
|
+
# Get status string
|
|
559
|
+
Reputable.lookup_asn("15169")
|
|
560
|
+
# => "untrusted_block" or nil
|
|
561
|
+
|
|
562
|
+
# Full lookup with metadata
|
|
563
|
+
Reputable.lookup_reputation(:asn, "15169")
|
|
564
|
+
# => { status: "untrusted_block", reason: "datacenter_abuse", ... }
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
**Note:** ASNs are normalized automatically - both `"15169"` and `"AS15169"` work.
|
|
568
|
+
|
|
504
569
|
---
|
|
505
570
|
|
|
506
571
|
## User Verification & Trust Flow
|
|
@@ -15,7 +15,8 @@ module Reputable
|
|
|
15
15
|
:connect_timeout, :read_timeout, :write_timeout,
|
|
16
16
|
:ssl_params, :trusted_proxies, :ip_header_priority,
|
|
17
17
|
:on_error, :trusted_keys, :base_url,
|
|
18
|
-
:site_name, :support_email, :support_url
|
|
18
|
+
:site_name, :support_email, :support_url,
|
|
19
|
+
:asn_fallback, :asn_header
|
|
19
20
|
|
|
20
21
|
# Alias for backward compatibility
|
|
21
22
|
alias_method :verification_base_url, :base_url
|
|
@@ -82,6 +83,10 @@ module Reputable
|
|
|
82
83
|
@site_name = ENV["REPUTABLE_SITE_NAME"]
|
|
83
84
|
@support_email = ENV["REPUTABLE_SUPPORT_EMAIL"]
|
|
84
85
|
@support_url = ENV["REPUTABLE_SUPPORT_URL"]
|
|
86
|
+
|
|
87
|
+
# ASN fallback: when IP has no reputation, check ASN reputation
|
|
88
|
+
@asn_fallback = env_truthy?("REPUTABLE_ASN_FALLBACK")
|
|
89
|
+
@asn_header = ENV.fetch("REPUTABLE_ASN_HEADER", "HTTP_X_ASN")
|
|
85
90
|
end
|
|
86
91
|
|
|
87
92
|
# Alias for backward compatibility
|
|
@@ -158,5 +163,20 @@ module Reputable
|
|
|
158
163
|
rescue IPAddr::InvalidAddressError
|
|
159
164
|
false
|
|
160
165
|
end
|
|
166
|
+
|
|
167
|
+
# Check if ASN fallback is enabled
|
|
168
|
+
def asn_fallback?
|
|
169
|
+
@asn_fallback
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
private
|
|
173
|
+
|
|
174
|
+
# Helper to check if an environment variable is truthy
|
|
175
|
+
def env_truthy?(name)
|
|
176
|
+
value = ENV[name]
|
|
177
|
+
return false if value.nil?
|
|
178
|
+
|
|
179
|
+
%w[1 true yes on enabled].include?(value.to_s.downcase)
|
|
180
|
+
end
|
|
161
181
|
end
|
|
162
182
|
end
|
data/lib/reputable/middleware.rb
CHANGED
|
@@ -60,6 +60,7 @@ module Reputable
|
|
|
60
60
|
@blocked_page_options = options.fetch(:blocked_page, {})
|
|
61
61
|
@blocked_page_path = options[:blocked_page_path]
|
|
62
62
|
@ignore_xhr = options.fetch(:ignore_xhr, false)
|
|
63
|
+
@asn_fallback = options.key?(:asn_fallback) ? options[:asn_fallback] : nil
|
|
63
64
|
end
|
|
64
65
|
|
|
65
66
|
def call(env)
|
|
@@ -318,6 +319,17 @@ module Reputable
|
|
|
318
319
|
ip = extract_ip(env)
|
|
319
320
|
env["reputable.ip"] = ip
|
|
320
321
|
status = Reputable::Reputation.lookup_ip(ip)
|
|
322
|
+
|
|
323
|
+
# Fallback to ASN reputation if IP has no status and ASN fallback is enabled
|
|
324
|
+
if status.nil? && asn_fallback_enabled?
|
|
325
|
+
asn = extract_asn(env)
|
|
326
|
+
if asn
|
|
327
|
+
env["reputable.asn"] = asn
|
|
328
|
+
status = Reputable::Reputation.lookup_asn(asn)
|
|
329
|
+
env["reputable.reputation_source"] = "asn" if status
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
|
|
321
333
|
env["reputable.reputation_status"] = status
|
|
322
334
|
env["reputable.ignore_analytics"] = status.to_s.start_with?("untrusted")
|
|
323
335
|
status
|
|
@@ -327,6 +339,28 @@ module Reputable
|
|
|
327
339
|
nil
|
|
328
340
|
end
|
|
329
341
|
|
|
342
|
+
def asn_fallback_enabled?
|
|
343
|
+
# Middleware option takes precedence, then config
|
|
344
|
+
return @asn_fallback unless @asn_fallback.nil?
|
|
345
|
+
|
|
346
|
+
Reputable.configuration.asn_fallback?
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def extract_asn(env)
|
|
350
|
+
# First check if app explicitly set it
|
|
351
|
+
return env["reputable.asn"] if env["reputable.asn"]
|
|
352
|
+
|
|
353
|
+
# Then check the configured header
|
|
354
|
+
header = Reputable.configuration.asn_header
|
|
355
|
+
value = env[header]
|
|
356
|
+
return nil if value.nil? || value.empty?
|
|
357
|
+
|
|
358
|
+
# Normalize: strip "AS" prefix if present
|
|
359
|
+
value.to_s.strip.sub(/^AS/i, "")
|
|
360
|
+
rescue StandardError
|
|
361
|
+
nil
|
|
362
|
+
end
|
|
363
|
+
|
|
330
364
|
def blocked_page_options
|
|
331
365
|
config = Reputable.configuration
|
|
332
366
|
defaults = {
|
data/lib/reputable/reputation.rb
CHANGED
|
@@ -205,8 +205,113 @@ module Reputable
|
|
|
205
205
|
lookup_ip(ip) == "untrusted_challenge"
|
|
206
206
|
end
|
|
207
207
|
|
|
208
|
+
# ========================================================================
|
|
209
|
+
# ASN LOOKUP METHODS
|
|
210
|
+
# ========================================================================
|
|
211
|
+
|
|
212
|
+
# Quick lookup for ASN reputation status
|
|
213
|
+
# Returns just the status string (or nil)
|
|
214
|
+
# ASN is normalized (strips "AS" prefix if present)
|
|
215
|
+
#
|
|
216
|
+
# @param asn [String] ASN (e.g., "15169" or "AS15169")
|
|
217
|
+
# @return [String, nil] Status string or nil
|
|
218
|
+
#
|
|
219
|
+
# @example
|
|
220
|
+
# status = Reputable::Reputation.lookup_asn("15169")
|
|
221
|
+
# # => "untrusted_block" or nil
|
|
222
|
+
def lookup_asn(asn)
|
|
223
|
+
result = lookup(:asn, normalize_asn(asn))
|
|
224
|
+
result&.dig(:status)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Check if an ASN is trusted (any trusted_* status)
|
|
228
|
+
#
|
|
229
|
+
# @param asn [String] ASN
|
|
230
|
+
# @return [Boolean] Returns false if disabled, nil lookup, or not trusted
|
|
231
|
+
def trusted_asn?(asn)
|
|
232
|
+
status = lookup_asn(asn)
|
|
233
|
+
return false if status.nil?
|
|
234
|
+
|
|
235
|
+
status.start_with?("trusted")
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Check if an ASN should be blocked
|
|
239
|
+
#
|
|
240
|
+
# @param asn [String] ASN
|
|
241
|
+
# @return [Boolean]
|
|
242
|
+
def blocked_asn?(asn)
|
|
243
|
+
lookup_asn(asn) == "untrusted_block"
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Check if an ASN should be challenged
|
|
247
|
+
#
|
|
248
|
+
# @param asn [String] ASN
|
|
249
|
+
# @return [Boolean]
|
|
250
|
+
def challenged_asn?(asn)
|
|
251
|
+
lookup_asn(asn) == "untrusted_challenge"
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# ========================================================================
|
|
255
|
+
# ASN APPLY CONVENIENCE METHODS
|
|
256
|
+
# ========================================================================
|
|
257
|
+
|
|
258
|
+
# Convenience method: Trust an ASN (behavioral by default)
|
|
259
|
+
def trust_asn(asn, reason: "manual_trust", status: :trusted_behavior, ttl: nil, **metadata)
|
|
260
|
+
apply(
|
|
261
|
+
entity_type: :asn,
|
|
262
|
+
entity_id: normalize_asn(asn),
|
|
263
|
+
status: status,
|
|
264
|
+
reason: reason,
|
|
265
|
+
ttl: ttl,
|
|
266
|
+
metadata: metadata
|
|
267
|
+
)
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# Convenience method: Block an ASN
|
|
271
|
+
def block_asn(asn, reason: "manual_block", ttl: nil, **metadata)
|
|
272
|
+
apply(
|
|
273
|
+
entity_type: :asn,
|
|
274
|
+
entity_id: normalize_asn(asn),
|
|
275
|
+
status: :untrusted_block,
|
|
276
|
+
reason: reason,
|
|
277
|
+
ttl: ttl,
|
|
278
|
+
metadata: metadata
|
|
279
|
+
)
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# Convenience method: Challenge an ASN (CAPTCHA, etc.)
|
|
283
|
+
def challenge_asn(asn, reason: "suspicious_traffic", ttl: nil, **metadata)
|
|
284
|
+
apply(
|
|
285
|
+
entity_type: :asn,
|
|
286
|
+
entity_id: normalize_asn(asn),
|
|
287
|
+
status: :untrusted_challenge,
|
|
288
|
+
reason: reason,
|
|
289
|
+
ttl: ttl,
|
|
290
|
+
metadata: metadata
|
|
291
|
+
)
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# Convenience method: Mark ASN to be ignored in analytics
|
|
295
|
+
def ignore_asn(asn, reason: "datacenter_traffic", ttl: nil, **metadata)
|
|
296
|
+
apply(
|
|
297
|
+
entity_type: :asn,
|
|
298
|
+
entity_id: normalize_asn(asn),
|
|
299
|
+
status: :untrusted_ignore,
|
|
300
|
+
reason: reason,
|
|
301
|
+
ttl: ttl,
|
|
302
|
+
metadata: metadata
|
|
303
|
+
)
|
|
304
|
+
end
|
|
305
|
+
|
|
208
306
|
private
|
|
209
307
|
|
|
308
|
+
# Normalize ASN by stripping "AS" prefix if present
|
|
309
|
+
# @param asn [String] ASN with or without prefix
|
|
310
|
+
# @return [String] ASN without prefix
|
|
311
|
+
def normalize_asn(asn)
|
|
312
|
+
asn.to_s.sub(/^AS/i, "")
|
|
313
|
+
end
|
|
314
|
+
|
|
210
315
|
def valid_entity_type?(entity_type)
|
|
211
316
|
VALID_ENTITY_TYPES.include?(entity_type.to_sym)
|
|
212
317
|
end
|
data/lib/reputable/version.rb
CHANGED
data/lib/reputable.rb
CHANGED
|
@@ -126,6 +126,39 @@ module Reputable
|
|
|
126
126
|
Reputation.challenged_ip?(ip)
|
|
127
127
|
end
|
|
128
128
|
|
|
129
|
+
# Delegate ASN reputation methods to Reputation module
|
|
130
|
+
def lookup_asn(asn)
|
|
131
|
+
Reputation.lookup_asn(asn)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def trusted_asn?(asn)
|
|
135
|
+
Reputation.trusted_asn?(asn)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def blocked_asn?(asn)
|
|
139
|
+
Reputation.blocked_asn?(asn)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def challenged_asn?(asn)
|
|
143
|
+
Reputation.challenged_asn?(asn)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def trust_asn(asn, reason: "manual_trust", status: :trusted_behavior, ttl: nil, **metadata)
|
|
147
|
+
Reputation.trust_asn(asn, reason: reason, status: status, ttl: ttl, **metadata)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def block_asn(asn, reason: "manual_block", ttl: nil, **metadata)
|
|
151
|
+
Reputation.block_asn(asn, reason: reason, ttl: ttl, **metadata)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def challenge_asn(asn, reason: "suspicious_traffic", ttl: nil, **metadata)
|
|
155
|
+
Reputation.challenge_asn(asn, reason: reason, ttl: ttl, **metadata)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def ignore_asn(asn, reason: "datacenter_traffic", ttl: nil, **metadata)
|
|
159
|
+
Reputation.ignore_asn(asn, reason: reason, ttl: ttl, **metadata)
|
|
160
|
+
end
|
|
161
|
+
|
|
129
162
|
# Generate a signed verification URL
|
|
130
163
|
# @param return_url [String] URL to redirect to after successful verification
|
|
131
164
|
# @param failure_url [String, nil] URL to redirect to on failure (optional)
|
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.18
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Reputable
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-02-03 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: redis
|