openasn 0.2.0 → 0.3.1
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/CHANGELOG.md +43 -0
- data/README.md +29 -9
- data/lib/generators/openasn/install/install_generator.rb +31 -9
- data/lib/generators/openasn/install/templates/openasn.rb +1 -1
- data/lib/openasn/parsers.rb +18 -7
- data/lib/openasn/result.rb +42 -1
- data/lib/openasn/version.rb +1 -1
- data/lib/openasn.rb +12 -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: 9fcde0e4605b2400824b5e03518b626465621cf43418d647a5b55d937cd66f38
|
|
4
|
+
data.tar.gz: beeee7a904311dd4a796c3e8b6306344b92e12d3410d5393dbc15bb3f56e9b02
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 659b36d6540e8e625e665c8030cc99c73c2c0ff832270a206e4096d2a2ec5ee3428423dca27cf689f0af30606612dc51a9dbf5858400ecc8f5dd91af7fff88b0
|
|
7
|
+
data.tar.gz: be56f2a505e3c127ae65f1caed99f1aadcdf521a7d94e426f82a3a20e9bd8871e5478148905830a1bb01852c327b993462079829228e306f953d4906a69dcc20
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,46 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.1] - 2026-07-07
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- `rails generate openasn:install` now inserts `OpenASN::UpdateJob` under an
|
|
8
|
+
existing `production:` entry in `config/recurring.yml` instead of appending
|
|
9
|
+
a duplicate top-level key that can make YAML parsers drop existing jobs.
|
|
10
|
+
- README scheduling guidance now calls out that manual update jobs should run
|
|
11
|
+
after the 03:17 UTC data build, using UTC to avoid daylight-saving drift.
|
|
12
|
+
- Removed the top-level `rexml/document` require from the WLVPN Tier B parser
|
|
13
|
+
so production Ruby 3.4 bundles that exclude the `rexml` bundled gem can boot
|
|
14
|
+
and precompile assets without adding app-side dependencies.
|
|
15
|
+
|
|
16
|
+
## [0.3.0] - 2026-07-07
|
|
17
|
+
|
|
18
|
+
Ergonomics release, driven by dogfooding the analytics/enrichment use case
|
|
19
|
+
(surfacing IP origin in admin panels) in a production Rails app.
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- `Result#label` — the verdict as a short human-readable string
|
|
24
|
+
("Residential ISP", "Hosting / datacenter", "Privacy relay") for admin
|
|
25
|
+
tables, tooltips, and log lines. One label per verdict, same append-only
|
|
26
|
+
contract as the enum.
|
|
27
|
+
- `Result#flag_names` — the ASN-level flag bitfield decoded to symbols
|
|
28
|
+
(`[:bad_asn, :vpn_provider]`), plus `Result#flag?(name)` and
|
|
29
|
+
`Result#bad_asn?` sugar. No more bit arithmetic to answer "is this ASN in
|
|
30
|
+
bad-asn-list?".
|
|
31
|
+
- `OpenASN.try_lookup(ip)` — nil-safe lookup: returns `nil` on nil/blank/
|
|
32
|
+
unparseable input instead of raising. The right call site for views and
|
|
33
|
+
analytics over historical data, where a garbage IP is data, not an
|
|
34
|
+
exception.
|
|
35
|
+
- `Result#to_h` now includes `flag_names` (append-only key addition; the
|
|
36
|
+
raw `flags` integer was useless in a log line).
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
|
|
40
|
+
- README reframed analytics-first: enriching admin panels/audit trails is
|
|
41
|
+
the primary documented use case; acting on the signal (step-up
|
|
42
|
+
verification, rate limits) is the optional later step.
|
|
43
|
+
|
|
3
44
|
## [0.2.0] - 2026-07-06
|
|
4
45
|
|
|
5
46
|
### Added
|
|
@@ -48,5 +89,7 @@
|
|
|
48
89
|
ranges, provider-attributed VPN overlays), Rack middleware, Rails install
|
|
49
90
|
generator.
|
|
50
91
|
|
|
92
|
+
[0.3.1]: https://github.com/openasn/openasn-ruby/compare/v0.3.0...v0.3.1
|
|
93
|
+
[0.3.0]: https://github.com/openasn/openasn-ruby/compare/v0.2.0...v0.3.0
|
|
51
94
|
[0.2.0]: https://github.com/openasn/openasn-ruby/compare/v0.1.0...v0.2.0
|
|
52
95
|
[0.1.0]: https://github.com/openasn/openasn-ruby/releases/tag/v0.1.0
|
data/README.md
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
result = OpenASN.lookup(request.remote_ip)
|
|
12
12
|
|
|
13
13
|
result.verdict # => :residential_isp
|
|
14
|
+
result.label # => "Residential ISP" — human-readable, for admin UIs & logs
|
|
14
15
|
result.infrastructure? # => false (true only for :hosting / :vpn / :tor_exit)
|
|
15
16
|
result.likely_human? # => true (:residential_isp, :mobile, :relay, :cgnat, :enterprise_gateway)
|
|
16
17
|
result.asn # => 3352
|
|
@@ -18,10 +19,10 @@ result.as_org # => "TELEFONICA DE ESPANA S.A.U."
|
|
|
18
19
|
result.sources # => [:asn_category] — every verdict is auditable
|
|
19
20
|
```
|
|
20
21
|
|
|
21
|
-
It
|
|
22
|
+
It answers, at zero marginal cost, the question every product eventually asks: **where do my signups, logins, and checkouts actually come from?** Enrich your admin panel, analytics, and audit trails with network origin — and if you ever decide to *act* on the signal, the same verdict drives step-up verification or rate limits. It's the data that costs $79–$200/month from commercial IP-intelligence APIs, as free, open, auditable data — compiled nightly from legally clean sources by the [OpenASN data project](https://github.com/openasn/openasn), with a data seed bundled right in the gem so it works on first boot, offline.
|
|
22
23
|
|
|
23
24
|
> [!IMPORTANT]
|
|
24
|
-
> **What this is NOT.** A clean or `:residential_isp` verdict is **absence of evidence, not proof of innocence**. Residential proxies — malicious traffic exiting through real home IPs — are structurally hard to detect offline, and OpenASN does not claim to detect them. `:vpn`, `:hosting`, and `:tor_exit` are high-confidence; treat everything else as a signal, not a sentence. **Never hard-block `:relay`, `:cgnat`, or `:mobile` — those are real people.** OpenASN is a
|
|
25
|
+
> **What this is NOT.** A clean or `:residential_isp` verdict is **absence of evidence, not proof of innocence**. Residential proxies — malicious traffic exiting through real home IPs — are structurally hard to detect offline, and OpenASN does not claim to detect them. `:vpn`, `:hosting`, and `:tor_exit` are high-confidence; treat everything else as a signal, not a sentence. **Never hard-block `:relay`, `:cgnat`, or `:mobile` — those are real people.** OpenASN tells you what the network *is*; it is not a fraud engine, and a verdict is a fact to weigh, never a sentence to execute.
|
|
25
26
|
|
|
26
27
|
## How it works
|
|
27
28
|
|
|
@@ -58,6 +59,11 @@ rails generate openasn:install
|
|
|
58
59
|
|
|
59
60
|
The generator creates `config/initializers/openasn.rb` (documented defaults) and, if you're on Solid Queue, schedules the daily `OpenASN::UpdateJob` in `config/recurring.yml`. **No migrations, no database** — data lives in `storage/openasn/` as memory-mapped-style packed files.
|
|
60
61
|
|
|
62
|
+
If you wire the job yourself, schedule it after the OpenASN data build
|
|
63
|
+
publishes at **03:17 UTC**. The generator uses `every day at 4:12am UTC` on
|
|
64
|
+
purpose; local time zones with daylight saving can drift before the UTC build
|
|
65
|
+
and fetch yesterday's release.
|
|
66
|
+
|
|
61
67
|
Not on Rails? `OpenASN.lookup` works anywhere Ruby ≥ 3.1 runs; schedule `OpenASN.update!` daily with cron/whenever.
|
|
62
68
|
|
|
63
69
|
## The API
|
|
@@ -68,9 +74,14 @@ Not on Rails? `OpenASN.lookup` works anywhere Ruby ≥ 3.1 runs; schedule `OpenA
|
|
|
68
74
|
r = OpenASN.lookup("146.70.107.100") # aliases: OpenASN.check, OpenASN.[]
|
|
69
75
|
# Never nil. Raises OpenASN::InvalidIPError (an ArgumentError) on garbage input.
|
|
70
76
|
|
|
77
|
+
r = OpenASN.try_lookup(user.signup_ip) # nil-safe variant: nil/blank/garbage in,
|
|
78
|
+
# nil out — right for views and analytics
|
|
79
|
+
# over historical data
|
|
80
|
+
|
|
71
81
|
r.verdict # one of: :residential_isp :mobile :business :hosting :vpn
|
|
72
82
|
# :tor_exit :relay :enterprise_gateway :education
|
|
73
83
|
# :government :cgnat :private :unknown
|
|
84
|
+
r.label # "VPN" — the verdict as a short human-readable string
|
|
74
85
|
r.infrastructure? # :hosting | :vpn | :tor_exit — the honest, high-confidence boolean
|
|
75
86
|
r.likely_human? # :residential_isp | :mobile | :relay | :cgnat | :enterprise_gateway
|
|
76
87
|
r.vpn? r.hosting? r.tor? r.relay? r.mobile? r.private? r.cgnat?
|
|
@@ -81,6 +92,9 @@ r.category # "hosting" (raw upstream category)
|
|
|
81
92
|
r.network_role # "major_transit" (raw upstream routing role)
|
|
82
93
|
r.provider # "aws" | "ProtonVPN" | "iCloud Private Relay" | nil (overlay attribution)
|
|
83
94
|
r.sources # [:x4b_vpn] — which data layer decided; every verdict is auditable
|
|
95
|
+
r.flag_names # [:vpn_provider, :bad_asn] — ASN-level signals, decoded to names
|
|
96
|
+
r.bad_asn? # ASN is in brianhama/bad-asn-list (infrastructure signal, not an accusation)
|
|
97
|
+
r.flag?(:cdn) # named check for any flag in r.flag_names' vocabulary
|
|
84
98
|
r.context_flags # [:cloudflare_range] — context that never decides verdicts
|
|
85
99
|
r.unrouted? # true when no ASN announces this IP
|
|
86
100
|
r.flags # raw u16 for power users (see the data project's FORMAT.md)
|
|
@@ -103,13 +117,14 @@ OpenASN returns one object, but its fields answer different questions:
|
|
|
103
117
|
| `sources` | Which rule/data layer decided the verdict | Best debugging field. Log it. |
|
|
104
118
|
| `provider` | Provider attribution from an exact overlay hit (`"aws"`, `"azure"`, `"ProtonVPN"`, ...) | Display/log it when present; nil is normal. |
|
|
105
119
|
| `context_flags` | Extra context that never decides the verdict | Useful for policy experiments; do not block solely on it. |
|
|
106
|
-
| `
|
|
120
|
+
| `flag_names` | ASN-level signals, decoded (`:bad_asn`, `:vpn_provider`, `:mobile_carrier`, `:enterprise_gw`, `:cdn`, `:hosting_extra`) | Display/log them; `bad_asn?` and `flag?(:name)` are the sugar. |
|
|
121
|
+
| `flags` | Packed low-level artifact bits | Wire detail; prefer `flag_names` in app code. |
|
|
107
122
|
|
|
108
123
|
Common examples:
|
|
109
124
|
|
|
110
125
|
- A DIGI Spain home IP is usually `category: "isp"` and `verdict: :residential_isp`. That is expected: `category` describes the ASN; `verdict` is OpenASN's safer application-level label.
|
|
111
126
|
- An Amazon IP may be `category: "hosting"`, `sources: [:x4b_dc]`, and with Tier B enabled `provider: "aws"`. The verdict remains `:hosting`; Tier B only improves attribution.
|
|
112
|
-
- `bad_asn
|
|
127
|
+
- `bad_asn?` is not an accusation and not a verdict. It means the ASN appears in `brianhama/bad-asn-list`, a curated MIT-licensed hosting/cloud/colo ASN list. When that signal decides classification, `sources` includes `:asn_bad_asn`.
|
|
113
128
|
- `:residential_isp` means "known eyeball ISP, no stronger infrastructure signal found." It does **not** mean "safe user."
|
|
114
129
|
|
|
115
130
|
### Verdict cheat sheet
|
|
@@ -179,12 +194,16 @@ request.env["openasn.result"]&.infrastructure?
|
|
|
179
194
|
> [!WARNING]
|
|
180
195
|
> **The classic integration bug:** behind a proxy/CDN without trusted-proxy configuration, you'll classify your own load balancer on every request (everything comes back `:private` or your host's datacenter). Inside Rails the middleware uses `remote_ip` semantics (honors `config.action_dispatch.trusted_proxies`); make sure the real client IP reaches your app — e.g. with the `cloudflare-rails` gem when behind Cloudflare.
|
|
181
196
|
|
|
182
|
-
## Recommended rollout:
|
|
197
|
+
## Recommended rollout: enrich first, act later (maybe never)
|
|
183
198
|
|
|
184
|
-
|
|
199
|
+
Start where the value is certain and the risk is zero: **visibility**. Put the verdict next to every IP your team already looks at. For many apps that's the whole integration — and it's how we dogfood it ourselves.
|
|
185
200
|
|
|
186
201
|
```ruby
|
|
187
|
-
# 1.
|
|
202
|
+
# 1. ENRICH — pure analytics, blocks nothing, breaks nothing:
|
|
203
|
+
# surface origin in your admin panel and audit trails…
|
|
204
|
+
OpenASN.try_lookup(user.signup_ip)&.label # "Residential ISP" / "VPN" / "Hosting / datacenter"
|
|
205
|
+
|
|
206
|
+
# …and/or log it on the flows you care about:
|
|
188
207
|
class RegistrationsController < ApplicationController
|
|
189
208
|
def create
|
|
190
209
|
Rails.logger.info(openasn: OpenASN.lookup(request.remote_ip).to_h.merge(flow: "signup"))
|
|
@@ -192,8 +211,9 @@ class RegistrationsController < ApplicationController
|
|
|
192
211
|
end
|
|
193
212
|
end
|
|
194
213
|
|
|
195
|
-
# 2.
|
|
196
|
-
# step-up verification (email confirm, captcha,
|
|
214
|
+
# 2. If (and only if) your OWN data says you have an abuse problem worth the
|
|
215
|
+
# friction, act — prefer step-up verification (email confirm, captcha,
|
|
216
|
+
# phone) over hard blocks:
|
|
197
217
|
Rack::Attack.blocklist("openasn: infrastructure on signup") do |req|
|
|
198
218
|
req.post? && req.path == "/signup" && OpenASN.lookup(req.ip).infrastructure?
|
|
199
219
|
end
|
|
@@ -12,6 +12,14 @@ module Openasn
|
|
|
12
12
|
class InstallGenerator < Rails::Generators::Base
|
|
13
13
|
source_root File.expand_path("templates", __dir__)
|
|
14
14
|
|
|
15
|
+
RECURRING_JOB = <<~YAML
|
|
16
|
+
# Added by `rails g openasn:install` — daily IP-origin data refresh
|
|
17
|
+
openasn_update:
|
|
18
|
+
class: OpenASN::UpdateJob
|
|
19
|
+
queue: default
|
|
20
|
+
schedule: every day at 4:12am UTC
|
|
21
|
+
YAML
|
|
22
|
+
|
|
15
23
|
def create_initializer
|
|
16
24
|
template "openasn.rb", "config/initializers/openasn.rb"
|
|
17
25
|
end
|
|
@@ -30,15 +38,8 @@ module Openasn
|
|
|
30
38
|
|
|
31
39
|
# 4:12am UTC: after the nightly data build (03:17 UTC) completes,
|
|
32
40
|
# off-hour to be kind to the Tier B upstreams.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
# Added by `rails g openasn:install` — daily IP-origin data refresh
|
|
36
|
-
production:
|
|
37
|
-
openasn_update:
|
|
38
|
-
class: OpenASN::UpdateJob
|
|
39
|
-
queue: default
|
|
40
|
-
schedule: every day at 4:12am UTC
|
|
41
|
-
YAML
|
|
41
|
+
recurring_path = File.expand_path(recurring, destination_root)
|
|
42
|
+
File.write(recurring_path, recurring_with_openasn_job(File.read(recurring_path)))
|
|
42
43
|
end
|
|
43
44
|
|
|
44
45
|
def display_post_install_message
|
|
@@ -63,6 +64,27 @@ module Openasn
|
|
|
63
64
|
say " and :relay/:cgnat/:mobile are real humans — never hard-block them.", :yellow
|
|
64
65
|
say ""
|
|
65
66
|
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def recurring_with_openasn_job(contents)
|
|
71
|
+
production = contents.match(/^production:\s*$/)
|
|
72
|
+
return "#{contents.rstrip}\n\nproduction:\n#{indent(RECURRING_JOB, 2)}" unless production
|
|
73
|
+
|
|
74
|
+
insert_at = next_environment_index(contents, production.end(0)) || contents.length
|
|
75
|
+
"#{contents[0...insert_at].rstrip}\n\n#{indent(RECURRING_JOB, 2)}#{contents[insert_at..]}"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def next_environment_index(contents, offset)
|
|
79
|
+
tail = contents[offset..]
|
|
80
|
+
match = tail.match(/\n(?=\S[^:\n]*:\s*$)/)
|
|
81
|
+
match && offset + match.begin(0)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def indent(text, spaces)
|
|
85
|
+
prefix = " " * spaces
|
|
86
|
+
text.lines.map { |line| line == "\n" ? line : "#{prefix}#{line}" }.join
|
|
87
|
+
end
|
|
66
88
|
end
|
|
67
89
|
end
|
|
68
90
|
end
|
|
@@ -18,7 +18,7 @@ OpenASN.configure do |config|
|
|
|
18
18
|
|
|
19
19
|
# Self-host or pin the data source if you need to:
|
|
20
20
|
# config.release_url = "https://github.com/openasn/openasn/releases/download/latest/"
|
|
21
|
-
# config.pin_version = "
|
|
21
|
+
# config.pin_version = "v2026.07.04" # a dated release tag, for reproducible builds
|
|
22
22
|
|
|
23
23
|
# Tier B overlays — fetched by YOUR server from the original authorities
|
|
24
24
|
# (Apple, Tor Project, AWS…), per the project's fetch-manifest. Defaults:
|
data/lib/openasn/parsers.rb
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
|
-
require "rexml/document"
|
|
5
4
|
require "zlib"
|
|
6
5
|
|
|
7
6
|
module OpenASN
|
|
@@ -219,13 +218,11 @@ module OpenASN
|
|
|
219
218
|
end
|
|
220
219
|
|
|
221
220
|
register "wlvpn_server_list_xml" do |body|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
doc.elements.each("//server") do |server|
|
|
225
|
-
next unless server.attributes["visible"].to_s == "1" && server.attributes["status"].to_s == "1"
|
|
221
|
+
tokens = xml_tag_attributes(body, "server").filter_map do |attrs|
|
|
222
|
+
next unless attrs["visible"].to_s == "1" && attrs["status"].to_s == "1"
|
|
226
223
|
|
|
227
|
-
ip =
|
|
228
|
-
|
|
224
|
+
ip = attrs["ip"].to_s.strip
|
|
225
|
+
ip unless ip.empty?
|
|
229
226
|
end
|
|
230
227
|
raise ParseError, "wlvpn_server_list_xml: no visible active server IPs — schema changed?" if tokens.empty?
|
|
231
228
|
|
|
@@ -433,6 +430,20 @@ module OpenASN
|
|
|
433
430
|
.strip
|
|
434
431
|
end
|
|
435
432
|
|
|
433
|
+
# Tiny tag-attribute scanner for provider XML-ish feeds. We only need
|
|
434
|
+
# exact attributes from a trusted source-specific tag; pulling in REXML
|
|
435
|
+
# would make production apps depend on a bundled gem that Ruby 3.4 no
|
|
436
|
+
# longer guarantees is installed.
|
|
437
|
+
def xml_tag_attributes(body, tag_name)
|
|
438
|
+
body.scan(/<#{Regexp.escape(tag_name)}\b[^>]*>/i).map do |tag|
|
|
439
|
+
attrs = {}
|
|
440
|
+
tag.scan(/\b([a-z_:][\w:.-]*)\s*=\s*(["'])(.*?)\2/im).each do |key, _quote, value|
|
|
441
|
+
attrs[key.downcase] = value
|
|
442
|
+
end
|
|
443
|
+
attrs
|
|
444
|
+
end
|
|
445
|
+
end
|
|
446
|
+
|
|
436
447
|
def openvpn_remote_hosts(content)
|
|
437
448
|
content.each_line.filter_map do |line|
|
|
438
449
|
match = line.match(/\Aremote\s+([^\s]+)(?:\s|$)/i)
|
data/lib/openasn/result.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "binary_format"
|
|
4
|
+
|
|
3
5
|
module OpenASN
|
|
4
6
|
# The answer to "where is this IP really coming from?". Immutable.
|
|
5
7
|
#
|
|
@@ -31,8 +33,29 @@ module OpenASN
|
|
|
31
33
|
# app decides those.
|
|
32
34
|
LIKELY_HUMAN_VERDICTS = %i[residential_isp mobile relay cgnat enterprise_gateway].freeze
|
|
33
35
|
|
|
36
|
+
# Short human-readable names for every verdict — what you'd print in an
|
|
37
|
+
# admin table or a log line a human reads. Deliberately neutral wording
|
|
38
|
+
# (no "suspicious", no "risky"): the label states what the network IS;
|
|
39
|
+
# what to do about it is your app's policy. Keys follow the VERDICTS
|
|
40
|
+
# append-only contract.
|
|
41
|
+
LABELS = {
|
|
42
|
+
residential_isp: "Residential ISP",
|
|
43
|
+
mobile: "Mobile carrier",
|
|
44
|
+
business: "Business",
|
|
45
|
+
hosting: "Hosting / datacenter",
|
|
46
|
+
vpn: "VPN",
|
|
47
|
+
tor_exit: "Tor exit",
|
|
48
|
+
relay: "Privacy relay",
|
|
49
|
+
enterprise_gateway: "Corporate gateway",
|
|
50
|
+
education: "University / research",
|
|
51
|
+
government: "Government",
|
|
52
|
+
cgnat: "Carrier NAT",
|
|
53
|
+
private: "Private IP",
|
|
54
|
+
unknown: "Unknown"
|
|
55
|
+
}.freeze
|
|
56
|
+
|
|
34
57
|
attr_reader :ip, :verdict, :asn, :as_org, :category, :network_role,
|
|
35
|
-
:provider, :sources, :flags, :context_flags
|
|
58
|
+
:provider, :sources, :flags, :context_flags, :flag_names
|
|
36
59
|
|
|
37
60
|
def initialize(ip:, verdict:, asn: nil, as_org: nil, category: nil,
|
|
38
61
|
network_role: nil, provider: nil, sources: [], flags: 0,
|
|
@@ -46,14 +69,30 @@ module OpenASN
|
|
|
46
69
|
@provider = provider
|
|
47
70
|
@sources = sources.freeze
|
|
48
71
|
@flags = flags
|
|
72
|
+
# The raw u16 is a wire detail; apps want names. Decoded once here so
|
|
73
|
+
# nobody downstream needs BinaryFormat bit knowledge.
|
|
74
|
+
@flag_names = BinaryFormat.flag_names(flags).freeze
|
|
49
75
|
@context_flags = context_flags.freeze
|
|
50
76
|
@unrouted = unrouted
|
|
51
77
|
freeze
|
|
52
78
|
end
|
|
53
79
|
|
|
80
|
+
# The verdict as a short human-readable string ("Residential ISP",
|
|
81
|
+
# "Hosting / datacenter") — for admin tables, tooltips, log lines.
|
|
82
|
+
def label = LABELS.fetch(verdict)
|
|
83
|
+
|
|
54
84
|
def infrastructure? = INFRASTRUCTURE_VERDICTS.include?(verdict)
|
|
55
85
|
def likely_human? = LIKELY_HUMAN_VERDICTS.include?(verdict)
|
|
56
86
|
|
|
87
|
+
# Named ASN-level flag check: flag?(:bad_asn), flag?(:cdn), … (the full
|
|
88
|
+
# vocabulary is BinaryFormat::FLAG_NAMES.values / the README table).
|
|
89
|
+
def flag?(name) = flag_names.include?(name)
|
|
90
|
+
|
|
91
|
+
# The ASN appears in brianhama/bad-asn-list — a curated catalog of
|
|
92
|
+
# hosting/cloud/colo ASNs. An infrastructure signal worth an admin's
|
|
93
|
+
# glance, NOT a verdict override and NOT a claim of abuse.
|
|
94
|
+
def bad_asn? = flag?(:bad_asn)
|
|
95
|
+
|
|
57
96
|
def vpn? = verdict == :vpn
|
|
58
97
|
def hosting? = verdict == :hosting
|
|
59
98
|
def tor? = verdict == :tor_exit
|
|
@@ -67,6 +106,7 @@ module OpenASN
|
|
|
67
106
|
|
|
68
107
|
# Everything, for logging and shadow mode. Stable keys — CarHey-style
|
|
69
108
|
# shadow analyses depend on this shape staying append-only.
|
|
109
|
+
# (flag_names added in 0.3.0: the raw bitfield is useless in a log line.)
|
|
70
110
|
def to_h
|
|
71
111
|
{
|
|
72
112
|
ip: ip,
|
|
@@ -80,6 +120,7 @@ module OpenASN
|
|
|
80
120
|
provider: provider,
|
|
81
121
|
sources: sources,
|
|
82
122
|
flags: flags,
|
|
123
|
+
flag_names: flag_names,
|
|
83
124
|
context_flags: context_flags,
|
|
84
125
|
unrouted: unrouted?
|
|
85
126
|
}
|
data/lib/openasn/version.rb
CHANGED
data/lib/openasn.rb
CHANGED
|
@@ -72,6 +72,18 @@ module OpenASN
|
|
|
72
72
|
alias check lookup
|
|
73
73
|
alias [] lookup
|
|
74
74
|
|
|
75
|
+
# Like lookup, but returns nil on nil/blank/unparseable input instead of
|
|
76
|
+
# raising — for analytics and rendering call sites where a missing or
|
|
77
|
+
# garbage IP is ordinary data (an old DB row, a stripped header), not an
|
|
78
|
+
# exceptional condition worth a begin/rescue at every call site.
|
|
79
|
+
def try_lookup(ip)
|
|
80
|
+
return nil if ip.nil? || (ip.is_a?(String) && ip.strip.empty?)
|
|
81
|
+
|
|
82
|
+
lookup(ip)
|
|
83
|
+
rescue InvalidIPError
|
|
84
|
+
nil
|
|
85
|
+
end
|
|
86
|
+
|
|
75
87
|
# Refresh canonical artifacts + Tier B overlays now, atomically
|
|
76
88
|
# swapping the in-memory dataset on success.
|
|
77
89
|
# -> :updated | :tier_b_only | :unchanged | :locked
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: openasn
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- rameerez
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-
|
|
10
|
+
date: 2026-08-08 00:00:00.000000000 Z
|
|
11
11
|
dependencies: []
|
|
12
12
|
description: 'OpenASN classifies where an IP address is really coming from — residential
|
|
13
13
|
ISP, mobile carrier, hosting/datacenter, VPN, Tor exit, iCloud Private Relay, enterprise
|