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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8767ca87c7fdd5abc6584e738599020cbd6977bc6193371caeef1425eedb8dfd
4
- data.tar.gz: 5196f00349ec3b380bec72db3b98c5f09d08965d297e7f43a8c639b9d1eed3dd
3
+ metadata.gz: 40be87edb5494144f499f263572e00d98331cd07913239fca32b5ce71450df26
4
+ data.tar.gz: 63cfe644b94396a6d72b05a75a45076e8bc41196f6c4970307992e6260d6f86f
5
5
  SHA512:
6
- metadata.gz: 36285df97965903af8e913d3f0937969ae4e48399f0468fd2e00b22d3b0e7c6d50d51682d9ec82a31483729ca7f0d69d4d0ec47115b12dce62ff01948d18b3ec
7
- data.tar.gz: 6aea15486988795b5ce444581be796e33d15906c871f5cc28208d876b0604053603f455f8d16536b4879773991905f85882f56b4fed47d85b71b1ea9abb34a42
6
+ metadata.gz: ed8aee9a76c98611718621fde0c1dada4aff7449092c9fc6cd1990d545b91ee1641c169d841eea12b2b0eeb56edc399e2f8db612041565a2d90dbf76bad34d92
7
+ data.tar.gz: 5b2eeb4fb8ddbf12beeb989b0a33aa5f42352c6a131ca1211ad5475b19978ba8244dbf52504555256aa0d79eb58935ab23be05cd503405d11d35b835bca506c0
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- reputable (0.1.17)
4
+ reputable (0.1.18)
5
5
  connection_pool (~> 2.2)
6
6
  redis (>= 4.0, < 6.0)
7
7
 
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
@@ -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 = {
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Reputable
4
- VERSION = "0.1.17"
4
+ VERSION = "0.1.18"
5
5
  end
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.17
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-01-29 00:00:00.000000000 Z
11
+ date: 2026-02-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: redis