ipscanner-io 0.1.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: c8ea296db01a984ba8d7229954dc752f1daf4a8c53ac9316724a7ed563d89f49
4
+ data.tar.gz: 189505d178d0f44e8622cedc5104aeb84d9849a1c1d440051c9b75fa9c8bcb61
5
+ SHA512:
6
+ metadata.gz: a6a9d4b0640499a25f03939275d1d133fa709351a9159472da883d6860810f4445e67e3d44e3a53936cb00510931a843ebaf1d2a0afecedc4ed674723d5821a4
7
+ data.tar.gz: 66727a60ddf55a4499edbc3b4be1a8bcd3be7b05aafe8446d27055bbb274587e5d35925220aaa16790518568ae1ea1cca57da0bec4ab1023ba076812de6454dd
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 IPScanner
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,154 @@
1
+ # IPScanner Ruby
2
+
3
+ Official Ruby client for the [IPScanner](https://ipscanner.io) API.
4
+
5
+ Full API reference: https://ipscanner.io/api-documentation
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ gem install ipscanner-io
11
+ ```
12
+
13
+ Or in a Gemfile:
14
+
15
+ ```ruby
16
+ gem "ipscanner-io"
17
+ ```
18
+
19
+ Requires Ruby 3.0 or newer. No runtime dependencies.
20
+
21
+ ## Quick start
22
+
23
+ ```ruby
24
+ require "ipscanner"
25
+
26
+ client = IPScanner::Client.new(api_key: "your-api-key")
27
+
28
+ result = client.ip.lookup("8.8.8.8")
29
+ puts result["networkClass"]
30
+ puts result["purity"]["grade"]
31
+ ```
32
+
33
+ Every method returns the parsed JSON response as a Hash with the same string keys the API sends.
34
+
35
+ ## Usage
36
+
37
+ ### IP
38
+
39
+ ```ruby
40
+ client.ip.lookup("example.com") # IP, CIDR or hostname
41
+ client.ip.vpn("1.1.1.1")
42
+ client.ip.proxy("1.1.1.1")
43
+ client.ip.geo("2001:4860:4860::8888")
44
+ client.ip.asn("1.1.1.1")
45
+ client.ip.whois("example.com")
46
+ client.ip.history(limit: 50, verdict: "vpn")
47
+ client.ip.demo("1.1.1.1") # no key needed
48
+ client.ip.myip # no key needed
49
+ ```
50
+
51
+ ### Bulk
52
+
53
+ ```ruby
54
+ report = client.bulk.check(ips: ["1.1.1.1", "8.8.8.8"])
55
+ report = client.bulk.check(input: File.read("ips.txt"))
56
+ ```
57
+
58
+ `stream` reads the NDJSON response line by line. Pass a block, or call it without one to get an Enumerator.
59
+
60
+ ```ruby
61
+ client.bulk.stream(input: File.read("ips.txt")) do |event|
62
+ case event["type"]
63
+ when "result" then puts "#{event['ip']} #{event['grade']}"
64
+ when "error" then warn "#{event['input']}: #{event['reason']}"
65
+ when "done" then puts "finished: #{event['complete']}"
66
+ end
67
+ end
68
+ ```
69
+
70
+ Fields the server leaves out are filled with their zero value. The `done` event has an extra `"complete"` key that is `true` only when every address was processed.
71
+
72
+ ### Agentscan
73
+
74
+ ```ruby
75
+ client.agentscan.check(ip: "203.0.113.7", user_agent: request.user_agent, headers: { "accept-language" => "en" })
76
+ client.agentscan.verify(ip: "66.249.66.1", bot: "googlebot")
77
+ client.agentscan.batch([{ line: 1, ip: "203.0.113.7", user_agent: "curl/8.0" }])
78
+ client.agentscan.allowlist
79
+ client.agentscan.self_check
80
+ ```
81
+
82
+ ### Provenance
83
+
84
+ ```ruby
85
+ client.provenance.check(ip: "203.0.113.7", claimed_jurisdiction: "eu")
86
+ client.provenance.verify
87
+ client.provenance.verify_anchored
88
+ client.provenance.chain(limit: 100)
89
+ client.provenance.jurisdictions
90
+ csv = client.provenance.export(from: "2026-01-01", to: "2026-01-31")
91
+ ```
92
+
93
+ ### Account
94
+
95
+ ```ruby
96
+ client.account.limits
97
+ client.account.usage
98
+ ```
99
+
100
+ ### ASN directory
101
+
102
+ ```ruby
103
+ client.asn_directory.top(top: 20, by: "prefixes")
104
+ client.asn_directory.search("cloudflare", limit: 10)
105
+ client.asn_directory.get("AS15169")
106
+ ```
107
+
108
+ ### Crawlers
109
+
110
+ ```ruby
111
+ client.crawlers.list
112
+ ```
113
+
114
+ ## Errors
115
+
116
+ All errors inherit from `IPScanner::Error`.
117
+
118
+ | Class | When |
119
+ | --- | --- |
120
+ | `IPScanner::APIError` | Any non-2xx response. Has `status`, `code`, `message`, `details`, `body`. |
121
+ | `IPScanner::AuthenticationError` | 401 or 403 |
122
+ | `IPScanner::NotFoundError` | 404 |
123
+ | `IPScanner::RateLimitError` | 429. Adds `reason`, `retry_after`, `reset_at`, `limit`, `usage`, `remaining`, `needed`, `plan`, `upgrade_url`, `rate_limit`. |
124
+ | `IPScanner::ConnectionError` | The API could not be reached. |
125
+ | `IPScanner::TimeoutError` | The request timed out. |
126
+
127
+ ```ruby
128
+ begin
129
+ client.ip.vpn("1.1.1.1")
130
+ rescue IPScanner::RateLimitError => e
131
+ sleep e.retry_after if e.retry_after
132
+ rescue IPScanner::APIError => e
133
+ warn "#{e.status} #{e.code}: #{e.message}"
134
+ end
135
+ ```
136
+
137
+ GET requests are retried up to `max_retries` times on network errors and 502, 503 and 504 responses. POST requests and 429 responses are never retried.
138
+
139
+ ## Configuration
140
+
141
+ ```ruby
142
+ IPScanner::Client.new(
143
+ api_key: "your-api-key", # default: ENV["IPSCANNER_API_KEY"]
144
+ base_url: "https://ipscanner.io", # default: ENV["IPSCANNER_API_URL"] or https://ipscanner.io
145
+ timeout: 30, # seconds
146
+ max_retries: 2
147
+ )
148
+ ```
149
+
150
+ Keyless endpoints (`ip.demo`, `ip.myip`, `asn_directory`, `crawlers`) work without an API key.
151
+
152
+ ## Licence
153
+
154
+ MIT
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IPScanner
4
+ # Entry point for the IPScanner API.
5
+ class Client
6
+ DEFAULT_BASE_URL = "https://ipscanner.io"
7
+
8
+ attr_reader :ip, :bulk, :agentscan, :provenance, :account, :asn_directory, :crawlers
9
+
10
+ def initialize(api_key: nil, base_url: nil, timeout: 30, max_retries: 2)
11
+ api_key ||= ENV.fetch("IPSCANNER_API_KEY", nil)
12
+ base_url ||= ENV.fetch("IPSCANNER_API_URL", nil)
13
+ base_url = DEFAULT_BASE_URL if base_url.nil? || base_url.empty?
14
+ http = HTTP.new(api_key: api_key, base_url: base_url, timeout: timeout, max_retries: max_retries)
15
+
16
+ @ip = Resources::IP.new(http)
17
+ @bulk = Resources::Bulk.new(http)
18
+ @agentscan = Resources::Agentscan.new(http)
19
+ @provenance = Resources::Provenance.new(http)
20
+ @account = Resources::Account.new(http)
21
+ @asn_directory = Resources::AsnDirectory.new(http)
22
+ @crawlers = Resources::Crawlers.new(http)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IPScanner
4
+ # Base class for every error raised by this library.
5
+ class Error < StandardError; end
6
+
7
+ # Raised when the request could not reach the API.
8
+ class ConnectionError < Error; end
9
+
10
+ # Raised when the request timed out.
11
+ class TimeoutError < ConnectionError; end
12
+
13
+ # Raised for any non-2xx response.
14
+ class APIError < Error
15
+ attr_reader :status, :code, :details, :body
16
+
17
+ def initialize(status:, code:, message:, details: nil, body: nil)
18
+ super(message)
19
+ @status = status
20
+ @code = code
21
+ @details = details
22
+ @body = body
23
+ end
24
+ end
25
+
26
+ # Raised on 401 and 403 responses.
27
+ class AuthenticationError < APIError; end
28
+
29
+ # Raised on 404 responses.
30
+ class NotFoundError < APIError; end
31
+
32
+ # Raised on 429 responses.
33
+ class RateLimitError < APIError
34
+ attr_reader :reason, :retry_after, :reset_at, :limit, :usage, :remaining,
35
+ :needed, :plan, :upgrade_url, :rate_limit
36
+
37
+ def initialize(rate_limit: {}, **kwargs)
38
+ super(**kwargs)
39
+ data = body.is_a?(Hash) ? body : {}
40
+ upgrade = data["upgrade"].is_a?(Hash) ? data["upgrade"] : {}
41
+ @reason = data["reason"]
42
+ @retry_after = data["retryAfter"] || rate_limit[:retry_after]
43
+ @reset_at = data["resetAt"]
44
+ @limit = data["limit"]
45
+ @usage = data["usage"]
46
+ @remaining = data["remaining"]
47
+ @needed = data["needed"]
48
+ @plan = data["plan"]
49
+ @upgrade_url = upgrade["url"]
50
+ @rate_limit = rate_limit
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,190 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "net/http"
5
+ require "openssl"
6
+ require "uri"
7
+
8
+ module IPScanner
9
+ # Low-level transport shared by every resource.
10
+ class HTTP
11
+ RETRY_STATUSES = [502, 503, 504].freeze
12
+ STREAM_TIMEOUT = 300
13
+ NETWORK_ERRORS = [
14
+ SocketError, SystemCallError, IOError, EOFError, OpenSSL::SSL::SSLError
15
+ ].freeze
16
+ TIMEOUT_ERRORS = [Net::OpenTimeout, Net::ReadTimeout, Net::WriteTimeout].freeze
17
+
18
+ RATE_LIMIT_HEADERS = {
19
+ meter: ["X-RateLimit-Meter", :string],
20
+ limit: ["X-RateLimit-Limit", :int],
21
+ remaining: ["X-RateLimit-Remaining", :int],
22
+ reset: ["X-RateLimit-Reset", :int],
23
+ daily_limit: ["X-RateLimit-Daily-Limit", :int],
24
+ daily_remaining: ["X-RateLimit-Daily-Remaining", :int],
25
+ daily_reset: ["X-RateLimit-Daily-Reset", :int],
26
+ hourly_limit: ["X-RateLimit-Hourly-Limit", :int],
27
+ hourly_remaining: ["X-RateLimit-Hourly-Remaining", :int],
28
+ hourly_reset: ["X-RateLimit-Hourly-Reset", :int],
29
+ usage_percent: ["X-RateLimit-Usage-Percent", :int],
30
+ upgrade_hint: ["X-Quota-Upgrade-Hint", :string],
31
+ upgrade_url: ["X-Quota-Upgrade-Url", :string],
32
+ upgrade_plan: ["X-Quota-Upgrade-Plan", :string],
33
+ retry_after: ["Retry-After", :int]
34
+ }.freeze
35
+
36
+ def self.escape(segment)
37
+ segment.to_s.b.gsub(/[^A-Za-z0-9\-._~]/) { |c| format("%%%02X", c.ord) }
38
+ end
39
+
40
+ def initialize(api_key:, base_url:, timeout:, max_retries:)
41
+ @api_key = api_key
42
+ @base_url = base_url.chomp("/")
43
+ @timeout = timeout
44
+ @max_retries = max_retries
45
+ end
46
+
47
+ def get(path, query = nil, raw: false)
48
+ request(Net::HTTP::Get, path, query: query, raw: raw)
49
+ end
50
+
51
+ def post(path, body = {})
52
+ request(Net::HTTP::Post, path, body: body)
53
+ end
54
+
55
+ def stream(path, body)
56
+ uri = build_uri(path)
57
+ req = build_request(Net::HTTP::Post, uri, body)
58
+ req["Accept"] = "application/x-ndjson"
59
+ with_errors do
60
+ connection(uri, STREAM_TIMEOUT).start do |http|
61
+ http.request(req) do |res|
62
+ raise error_for(res, res.body) unless res.is_a?(Net::HTTPSuccess)
63
+
64
+ buffer = +""
65
+ res.read_body do |chunk|
66
+ buffer << chunk
67
+ while (newline = buffer.index("\n"))
68
+ line = buffer.slice!(0, newline + 1).strip
69
+ yield JSON.parse(line) unless line.empty?
70
+ end
71
+ end
72
+ rest = buffer.strip
73
+ yield JSON.parse(rest) unless rest.empty?
74
+ end
75
+ end
76
+ end
77
+ end
78
+
79
+ private
80
+
81
+ def request(klass, path, query: nil, body: nil, raw: false)
82
+ uri = build_uri(path, query)
83
+ attempts = 0
84
+ loop do
85
+ attempts += 1
86
+ begin
87
+ res = with_errors do
88
+ connection(uri, @timeout).start { |http| http.request(build_request(klass, uri, body)) }
89
+ end
90
+ rescue ConnectionError
91
+ raise unless retryable?(klass, attempts)
92
+
93
+ backoff(attempts)
94
+ next
95
+ end
96
+ if RETRY_STATUSES.include?(res.code.to_i) && retryable?(klass, attempts)
97
+ backoff(attempts)
98
+ next
99
+ end
100
+ raise error_for(res, res.body) unless res.is_a?(Net::HTTPSuccess)
101
+
102
+ return raw ? res.body.to_s : parse(res.body)
103
+ end
104
+ end
105
+
106
+ def retryable?(klass, attempts)
107
+ klass == Net::HTTP::Get && attempts <= @max_retries
108
+ end
109
+
110
+ def backoff(attempt)
111
+ sleep(0.5 * (2**(attempt - 1)))
112
+ end
113
+
114
+ def with_errors
115
+ yield
116
+ rescue *TIMEOUT_ERRORS => e
117
+ raise TimeoutError, e.message
118
+ rescue *NETWORK_ERRORS => e
119
+ raise ConnectionError, e.message
120
+ end
121
+
122
+ def build_uri(path, query = nil)
123
+ uri = URI.parse(@base_url + path)
124
+ params = (query || {}).compact
125
+ uri.query = URI.encode_www_form(params) unless params.empty?
126
+ uri
127
+ end
128
+
129
+ def connection(uri, timeout)
130
+ http = Net::HTTP.new(uri.host, uri.port)
131
+ http.use_ssl = uri.scheme == "https"
132
+ http.open_timeout = timeout
133
+ http.read_timeout = timeout
134
+ http.write_timeout = timeout
135
+ http
136
+ end
137
+
138
+ def build_request(klass, uri, body)
139
+ req = klass.new(uri)
140
+ req["Accept"] = "application/json"
141
+ req["User-Agent"] = "ipscanner-ruby/#{VERSION}"
142
+ req["Authorization"] = "Bearer #{@api_key}" if @api_key && !@api_key.empty?
143
+ unless body.nil?
144
+ req["Content-Type"] = "application/json"
145
+ req.body = JSON.generate(body)
146
+ end
147
+ req
148
+ end
149
+
150
+ def parse(text)
151
+ return nil if text.nil? || text.empty?
152
+
153
+ JSON.parse(text)
154
+ end
155
+
156
+ def error_for(res, text)
157
+ status = res.code.to_i
158
+ data = begin
159
+ JSON.parse(text.to_s)
160
+ rescue JSON::ParserError
161
+ nil
162
+ end
163
+ data = nil unless data.is_a?(Hash) && data["error"].is_a?(String)
164
+ code = data ? data["error"] : "http_#{status}"
165
+ message = data && data["message"] ? data["message"] : status_text(res, status)
166
+ args = { status: status, code: code, message: message, details: data && data["details"], body: data || text }
167
+
168
+ case status
169
+ when 401, 403 then AuthenticationError.new(**args)
170
+ when 404 then NotFoundError.new(**args)
171
+ when 429 then RateLimitError.new(rate_limit: rate_limit(res), **args)
172
+ else APIError.new(**args)
173
+ end
174
+ end
175
+
176
+ def status_text(res, status)
177
+ text = res.message.to_s.strip
178
+ text.empty? ? "HTTP #{status}" : text
179
+ end
180
+
181
+ def rate_limit(res)
182
+ RATE_LIMIT_HEADERS.each_with_object({}) do |(key, (header, type)), out|
183
+ value = res[header]
184
+ next if value.nil? || value.empty?
185
+
186
+ out[key] = type == :int ? Integer(value, exception: false) || value : value
187
+ end
188
+ end
189
+ end
190
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module IPScanner
6
+ module Resources
7
+ # Quota and usage for the current key.
8
+ class Account < Base
9
+ def limits
10
+ @http.get("/v1/user/limits")
11
+ end
12
+
13
+ def usage
14
+ @http.get("/v1/usage/summary")
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module IPScanner
6
+ module Resources
7
+ # Agentscan: classify the client behind a request.
8
+ class Agentscan < Base
9
+ def check(ip:, user_agent: nil, ja4: nil, headers: nil, headless_flags: nil, request_id: nil)
10
+ body = {
11
+ ip: ip, user_agent: user_agent, ja4: ja4, headers: headers,
12
+ headless_flags: headless_flags, request_id: request_id
13
+ }
14
+ @http.post("/v1/agentscan/check", compact(body))
15
+ end
16
+
17
+ # Classifies parsed log lines, each a Hash with line, ip and optional user_agent, ja4, headers.
18
+ def batch(lines)
19
+ @http.post("/v1/agentscan/batch", { lines: lines })
20
+ end
21
+
22
+ # Checks whether a request claiming to be a known crawler is genuine.
23
+ def verify(ip:, bot: nil, user_agent: nil)
24
+ @http.post("/v1/agentscan/verify", compact({ ip: ip, bot: bot, user_agent: user_agent }))
25
+ end
26
+
27
+ def allowlist
28
+ @http.get("/v1/agentscan/allowlist")
29
+ end
30
+
31
+ # Classifies the caller. Not metered.
32
+ def self_check(ip: nil, user_agent: nil, headers: nil, ja4: nil)
33
+ body = { ip: ip, user_agent: user_agent, headers: headers, ja4: ja4 }
34
+ @http.post("/v1/agentscan/self", compact(body))
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module IPScanner
6
+ module Resources
7
+ # Keyless directory of autonomous systems.
8
+ class AsnDirectory < Base
9
+ # Largest networks, ranked by addresses or prefixes.
10
+ def top(top: nil, by: nil)
11
+ @http.get("/v1/asn/directory", { top: top, by: by })
12
+ end
13
+
14
+ def search(query, limit: nil)
15
+ @http.get("/v1/asn/directory", { q: query, limit: limit })
16
+ end
17
+
18
+ # Accepts 15169 or "AS15169".
19
+ def get(asn)
20
+ number = asn.to_s.strip.sub(/\AAS/i, "")
21
+ @http.get("/v1/asn/directory/#{escape(number)}")
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IPScanner
4
+ module Resources
5
+ # Shared plumbing for resource classes.
6
+ class Base
7
+ def initialize(http)
8
+ @http = http
9
+ end
10
+
11
+ private
12
+
13
+ def escape(segment)
14
+ HTTP.escape(segment)
15
+ end
16
+
17
+ def compact(hash)
18
+ hash.compact
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module IPScanner
6
+ module Resources
7
+ # Many addresses per request.
8
+ class Bulk < Base
9
+ EVENT_DEFAULTS = {
10
+ "type" => "", "index" => 0, "total" => 0, "input" => "", "reason" => "",
11
+ "message" => "", "error" => "", "ip" => "", "verdict" => "", "classification" => "",
12
+ "confidence" => 0.0, "anonymized" => false, "score" => 0, "grade" => "",
13
+ "isTorExit" => false, "asn" => "", "asnName" => "", "asnType" => "", "country" => "",
14
+ "processed" => 0, "failed" => 0, "metered" => 0
15
+ }.freeze
16
+
17
+ # Scores a list of addresses or a pasted blob in one response.
18
+ def check(ips: nil, input: nil)
19
+ @http.post("/v1/bulk/check", body(ips, input))
20
+ end
21
+
22
+ # Yields each NDJSON event as it arrives, or returns an Enumerator without a block.
23
+ def stream(ips: nil, input: nil)
24
+ return enum_for(:stream, ips: ips, input: input) unless block_given?
25
+
26
+ @http.stream("/v1/ip/bulk", body(ips, input)) do |line|
27
+ event = EVENT_DEFAULTS.merge(line)
28
+ if event["type"] == "done"
29
+ event["complete"] = event["reason"] == "complete" &&
30
+ event["processed"] + event["failed"] >= event["total"]
31
+ end
32
+ yield event
33
+ end
34
+ nil
35
+ end
36
+
37
+ private
38
+
39
+ def body(ips, input)
40
+ compact({ ips: ips, input: input })
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module IPScanner
6
+ module Resources
7
+ # Keyless catalog of known crawlers.
8
+ class Crawlers < Base
9
+ def list
10
+ @http.get("/v1/crawlers")
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module IPScanner
6
+ module Resources
7
+ # Single-address lookups.
8
+ class IP < Base
9
+ # Full lookup for an IP, CIDR or hostname.
10
+ def lookup(target)
11
+ @http.post("/v1/ip/lookup", { target: target })
12
+ end
13
+
14
+ def vpn(ip)
15
+ @http.get("/v1/vpn/#{escape(ip)}")
16
+ end
17
+
18
+ def proxy(ip)
19
+ @http.get("/v1/proxy/#{escape(ip)}")
20
+ end
21
+
22
+ def geo(ip)
23
+ @http.get("/v1/geo/#{escape(ip)}")
24
+ end
25
+
26
+ def asn(ip)
27
+ @http.get("/v1/asn/#{escape(ip)}")
28
+ end
29
+
30
+ def whois(domain)
31
+ @http.get("/v1/whois/#{escape(domain)}")
32
+ end
33
+
34
+ # Recent lookups made by this account.
35
+ def history(limit: nil, before: nil, verdict: nil)
36
+ @http.get("/v1/ip/history", { limit: limit, before: before, verdict: verdict })
37
+ end
38
+
39
+ # Keyless lookup with the same response shape as lookup.
40
+ def demo(ip)
41
+ @http.get("/v1/demo/#{escape(ip)}")
42
+ end
43
+
44
+ # Describes the address the request came from.
45
+ def myip
46
+ @http.get("/v1/myip")
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base"
4
+
5
+ module IPScanner
6
+ module Resources
7
+ # Network provenance attestations.
8
+ class Provenance < Base
9
+ def check(ip:, claimed_jurisdiction: nil, request_context: nil)
10
+ body = { ip: ip, claimed_jurisdiction: claimed_jurisdiction, request_context: request_context }
11
+ @http.post("/v1/provenance/check", compact(body))
12
+ end
13
+
14
+ def verify
15
+ @http.get("/v1/provenance/verify")
16
+ end
17
+
18
+ def verify_anchored
19
+ @http.post("/v1/provenance/verify", {})
20
+ end
21
+
22
+ def chain(limit: nil, before: nil)
23
+ @http.get("/v1/provenance/chain", { limit: limit, before: before })
24
+ end
25
+
26
+ def jurisdictions
27
+ @http.get("/v1/provenance/jurisdictions")
28
+ end
29
+
30
+ # Returns the attestation log as a CSV string.
31
+ def export(from: nil, to: nil)
32
+ @http.get("/v1/provenance/export", { from: from, to: to }, raw: true)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IPScanner
4
+ VERSION = "0.1.0"
5
+ end
data/lib/ipscanner.rb ADDED
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "ipscanner/version"
4
+ require_relative "ipscanner/errors"
5
+ require_relative "ipscanner/http"
6
+ require_relative "ipscanner/resources/ip"
7
+ require_relative "ipscanner/resources/bulk"
8
+ require_relative "ipscanner/resources/agentscan"
9
+ require_relative "ipscanner/resources/provenance"
10
+ require_relative "ipscanner/resources/account"
11
+ require_relative "ipscanner/resources/asn_directory"
12
+ require_relative "ipscanner/resources/crawlers"
13
+ require_relative "ipscanner/client"
14
+
15
+ # Ruby client for the IPScanner API.
16
+ module IPScanner
17
+ end
metadata ADDED
@@ -0,0 +1,58 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ipscanner-io
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - IPScanner
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: 'Official Ruby client for the IPScanner API: IP lookups, bulk checks,
13
+ Agentscan and provenance.'
14
+ executables: []
15
+ extensions: []
16
+ extra_rdoc_files: []
17
+ files:
18
+ - LICENSE
19
+ - README.md
20
+ - lib/ipscanner.rb
21
+ - lib/ipscanner/client.rb
22
+ - lib/ipscanner/errors.rb
23
+ - lib/ipscanner/http.rb
24
+ - lib/ipscanner/resources/account.rb
25
+ - lib/ipscanner/resources/agentscan.rb
26
+ - lib/ipscanner/resources/asn_directory.rb
27
+ - lib/ipscanner/resources/base.rb
28
+ - lib/ipscanner/resources/bulk.rb
29
+ - lib/ipscanner/resources/crawlers.rb
30
+ - lib/ipscanner/resources/ip.rb
31
+ - lib/ipscanner/resources/provenance.rb
32
+ - lib/ipscanner/version.rb
33
+ homepage: https://ipscanner.io
34
+ licenses:
35
+ - MIT
36
+ metadata:
37
+ homepage_uri: https://ipscanner.io
38
+ source_code_uri: https://github.com/ipscanner/ipscanner-ruby
39
+ documentation_uri: https://ipscanner.io/api-documentation
40
+ rubygems_mfa_required: 'true'
41
+ rdoc_options: []
42
+ require_paths:
43
+ - lib
44
+ required_ruby_version: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '3.0'
49
+ required_rubygems_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ requirements: []
55
+ rubygems_version: 4.0.20
56
+ specification_version: 4
57
+ summary: Ruby client for the IPScanner API.
58
+ test_files: []