detector 0.1.1 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 78c814535b333de1c5e5688053248859109551711abf79dec4fb4c3aca723b5c
4
- data.tar.gz: 6589e309685233203306e23f8aa581470d2af49624f8adfd8643d063670e6cb2
3
+ metadata.gz: ab9800bbb6b9322ca7e8115b438c89cf5da96febc891e1ae6cf55ff8de6ff011
4
+ data.tar.gz: 4916567db9f219e16d9fe3bef938dbc51958d786205894a850074686c5e41400
5
5
  SHA512:
6
- metadata.gz: ac198d4ecc2dd09814e8bebbed2e0c5e165cd8f722bb374d3e5ba76ba61361036f7bcf071bffd26dac5ecb8a4ba4dfdca8fb29f92439ad8ea827b2ec82908344
7
- data.tar.gz: 5d3220c7780b78167c16fd43c1e3f663026f7ab4b471606b1b2db784eee741f9606bfe380180efca85d961cfab780f2a3f9deca273658a6b4e07d3e5d78e0acd
6
+ metadata.gz: aad9b58db57085e77b3543c50297d57d779506b93e0f89fd7475df0de19c3b45170127588dc7754fae9f5e256feb1939cb5aba88c98bc56d6289f9413f866ad6
7
+ data.tar.gz: 3dc3ef92bfe6bb083f90e282d15ab0866722da1dfaddb30b8cae8076150755fad0c31274649eef7e16a19b993fb07443d68d301eb236ffaa5c7beaf91c234216
data/README.md CHANGED
@@ -52,6 +52,14 @@ db.host # => "host"
52
52
  db.port # => 5432
53
53
  db.version # => "PostgreSQL 12.1 on x86_64-pc-linux-gnu, ..."
54
54
 
55
+ # Detect infrastructure
56
+ db.infrastructure # => "Amazon Web Services", "Google Cloud Platform", etc.
57
+
58
+ # Geographic information
59
+ db.geography # => "Ashburn, Virginia, United States"
60
+ db.region # => "us-east-1"
61
+ db.asn # => "AS16509"
62
+
55
63
  # Get database stats
56
64
  db.database_count # => 5
57
65
  db.databases # => [{ name: "db1", size: "1.2 GB", ... }, ...]
data/bin/detector CHANGED
@@ -4,6 +4,7 @@ require "bundler/setup"
4
4
  require "detector"
5
5
 
6
6
  if ARGV.empty?
7
+ puts "Detector v#{Detector::VERSION}"
7
8
  puts "Usage: detector <URI>"
8
9
  puts "Example: detector \"postgres://user:pass@host:port/dbname\""
9
10
  exit 1
@@ -17,10 +18,31 @@ if detector.nil?
17
18
  exit 1
18
19
  end
19
20
 
21
+ puts "Detector v#{Detector::VERSION}"
20
22
  puts "Detected: #{detector.kind}"
21
23
  puts "Version: #{detector.version}"
22
24
  puts "Host: #{detector.host}:#{detector.port}"
23
25
 
26
+ if detector.user_access_level
27
+ puts "User access level: #{detector.user_access_level}"
28
+ end
29
+
30
+ if detector.infrastructure
31
+ puts "Infrastructure: #{detector.infrastructure}"
32
+ end
33
+
34
+ if detector.geography
35
+ puts "Location: #{detector.geography}"
36
+ end
37
+
38
+ if detector.region
39
+ puts "Region: #{detector.region}"
40
+ end
41
+
42
+ if detector.asn
43
+ puts "ASN: #{detector.asn}"
44
+ end
45
+
24
46
  if detector.databases?
25
47
  db_count = detector.database_count
26
48
  puts "\nDatabases: #{db_count}"
@@ -80,6 +80,32 @@ module Detector
80
80
  def cli_name
81
81
  "mariadb"
82
82
  end
83
+
84
+ def user_access_level
85
+ # Start with MySQL access level check
86
+ access_level = super
87
+
88
+ # Add MariaDB-specific details if needed
89
+ return access_level unless connection
90
+
91
+ # Check for MariaDB-specific roles (MariaDB 10.0.5+)
92
+ begin
93
+ result = connection.query("SELECT 1 FROM information_schema.plugins WHERE plugin_name = 'ROLES'")
94
+ if result.count > 0
95
+ roles_result = connection.query("SELECT CURRENT_ROLE()").first
96
+ current_role = roles_result.values.first
97
+
98
+ # If a role is active, append it to the access level
99
+ if current_role && current_role != '' && current_role != 'NONE'
100
+ return "#{access_level} (Role: #{current_role})"
101
+ end
102
+ end
103
+ rescue => e
104
+ # Role system not available or not accessible to user
105
+ end
106
+
107
+ access_level
108
+ end
83
109
  end
84
110
  end
85
111
 
@@ -89,6 +89,67 @@ module Detector
89
89
  def cli_name
90
90
  "mysql"
91
91
  end
92
+
93
+ def user_access_level
94
+ return nil unless connection
95
+
96
+ # Get all privileges for current user
97
+ grants = []
98
+ begin
99
+ result = connection.query("SHOW GRANTS FOR CURRENT_USER()")
100
+ result.each do |row|
101
+ grants << row.values.first
102
+ end
103
+ rescue => e
104
+ return "Limited access (unable to check privileges)"
105
+ end
106
+
107
+ grant_text = grants.join(" ")
108
+
109
+ # Check for root/admin privileges
110
+ if grant_text =~ /ALL PRIVILEGES ON \*\.\* TO/i
111
+ return "Administrator (all privileges)"
112
+ end
113
+
114
+ # Check for global privileges
115
+ if grant_text =~ /GRANT .* ON \*\.\*/i
116
+ global_privs = []
117
+ global_privs << "CREATE USER" if grant_text =~ /CREATE USER/i
118
+ global_privs << "PROCESS" if grant_text =~ /PROCESS/i
119
+ global_privs << "SUPER" if grant_text =~ /SUPER/i
120
+ global_privs << "RELOAD" if grant_text =~ /RELOAD/i
121
+ global_privs << "SHUTDOWN" if grant_text =~ /SHUTDOWN/i
122
+
123
+ if global_privs.include?("CREATE USER") || global_privs.include?("SUPER")
124
+ return "Power user (#{global_privs.join(", ")})"
125
+ elsif !global_privs.empty?
126
+ return "System monitor (#{global_privs.join(", ")})"
127
+ end
128
+ end
129
+
130
+ # Check for database-level privileges
131
+ db_with_all = []
132
+ if grant_text =~ /ALL PRIVILEGES ON (`[^`]+`|\w+)\./i
133
+ db_name = $1.gsub(/`/, "")
134
+ db_with_all << db_name
135
+ end
136
+
137
+ if !db_with_all.empty?
138
+ return "Database admin (full access to: #{db_with_all.join(", ")})"
139
+ end
140
+
141
+ # Check for specific privileges
142
+ can_write = grant_text =~ /INSERT|UPDATE|DELETE|CREATE|ALTER|DROP/i
143
+ can_read = grant_text =~ /SELECT/i
144
+
145
+ if can_write
146
+ "Write access"
147
+ elsif can_read
148
+ "Read-only access"
149
+ else
150
+ "Limited access"
151
+ end
152
+ end
92
153
  end
93
154
  end
94
155
 
@@ -114,6 +114,46 @@ module Detector
114
114
  def cli_name
115
115
  "psql"
116
116
  end
117
+
118
+ def user_access_level
119
+ return nil unless connection
120
+
121
+ is_superuser = connection.exec("SELECT usesuper FROM pg_user WHERE usename = current_user").first["usesuper"] == "t" rescue false
122
+ is_replication = connection.exec("SELECT rolreplication FROM pg_roles WHERE rolname = current_user").first["rolreplication"] == "t" rescue false
123
+ roles = connection.exec("SELECT r.rolname FROM pg_roles r JOIN pg_auth_members m ON r.oid = m.roleid JOIN pg_roles u ON m.member = u.oid WHERE u.rolname = current_user").map { |row| row["rolname"] } rescue []
124
+
125
+ create_db = connection.exec("SELECT usecreatedb FROM pg_user WHERE usename = current_user").first["usecreatedb"] == "t" rescue false
126
+
127
+ if is_superuser
128
+ "Superuser (full access)"
129
+ elsif is_replication
130
+ "Replication user (system-level replication access)"
131
+ elsif create_db
132
+ "Database creator (can create new databases)"
133
+ elsif roles.include?("rds_superuser")
134
+ "RDS Superuser (limited admin privileges)"
135
+ else
136
+ # Check if can access system catalogs (higher than regular user)
137
+ begin
138
+ connection.exec("SELECT count(*) FROM pg_shadow")
139
+ "Power user (access to system catalogs)"
140
+ rescue => e
141
+ # Check if can create tables in current database
142
+ begin
143
+ connection.exec("CREATE TABLE __temp_access_check (id int); DROP TABLE __temp_access_check;")
144
+ "Regular user (table management)"
145
+ rescue => e
146
+ # Check for readonly access
147
+ begin
148
+ connection.exec("SELECT current_database()")
149
+ "Read-only user"
150
+ rescue => e
151
+ "Limited access"
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end
117
157
  end
118
158
  end
119
159
 
@@ -71,6 +71,82 @@ module Detector
71
71
  def cli_name
72
72
  "redis-cli"
73
73
  end
74
+
75
+ def user_access_level
76
+ return nil unless connection
77
+
78
+ # Redis 6.0+ supports ACLs, older versions just have auth or no auth
79
+ redis_version = info['redis_version'].to_s
80
+
81
+ if Gem::Version.new(redis_version) >= Gem::Version.new('6.0.0')
82
+ begin
83
+ acl_info = connection.call('ACL', 'LIST')
84
+ default_user = acl_info.grep(/default/).first
85
+
86
+ if default_user.include?('on') && default_user.include?('nopass')
87
+ return "Administrator (open access)"
88
+ elsif default_user.include?('on') && default_user.include?('~*')
89
+ return "Administrator (password protected)"
90
+ elsif default_user.include?('allkeys')
91
+ if default_user.include?('allcommands')
92
+ return "Full access (all commands, all keys)"
93
+ else
94
+ return "Limited command access (all keys)"
95
+ end
96
+ else
97
+ if default_user.include?('reset')
98
+ "No access (default rights)"
99
+ else
100
+ "Custom ACL pattern"
101
+ end
102
+ end
103
+ rescue => e
104
+ # Try to determine rights by test commands for older Redis
105
+ self.generic_redis_access_check
106
+ end
107
+ else
108
+ # Older Redis version
109
+ self.generic_redis_access_check
110
+ end
111
+ end
112
+
113
+ def generic_redis_access_check
114
+ # Check for admin commands
115
+ admin_access = false
116
+ begin
117
+ # Try an admin command (CONFIG GET)
118
+ connection.call('CONFIG', 'GET', 'maxmemory')
119
+ admin_access = true
120
+ rescue => e
121
+ admin_access = false
122
+ end
123
+
124
+ # Check for write ability
125
+ write_access = false
126
+ begin
127
+ # Use a random key name to avoid conflicts
128
+ test_key = "__test_key_#{rand(1000000)}"
129
+ connection.call('SET', test_key, 'test_value')
130
+ connection.call('DEL', test_key)
131
+ write_access = true
132
+ rescue => e
133
+ write_access = false
134
+ end
135
+
136
+ if admin_access
137
+ "Administrator (config access)"
138
+ elsif write_access
139
+ "Regular user (read/write)"
140
+ else
141
+ # Try a read command
142
+ begin
143
+ connection.call('PING')
144
+ "Read-only user"
145
+ rescue => e
146
+ "Limited access"
147
+ end
148
+ end
149
+ end
74
150
  end
75
151
  end
76
152
 
@@ -32,6 +32,28 @@ module Detector
32
32
  def cli_name
33
33
  "telnet"
34
34
  end
35
+
36
+ def user_access_level
37
+ return nil unless connection
38
+
39
+ # For SMTP, limited testing is possible
40
+ # We know we have authenticated access already if connection exists
41
+
42
+ # Try to validate recipient (admin-level feature)
43
+ admin_level = false
44
+ begin
45
+ connection.vrfy("postmaster")
46
+ admin_level = true
47
+ rescue => e
48
+ admin_level = false
49
+ end
50
+
51
+ if admin_level
52
+ "Administrator (VRFY command allowed)"
53
+ else
54
+ "Authenticated user (send mail)"
55
+ end
56
+ end
35
57
  end
36
58
  end
37
59
 
data/lib/detector/base.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require 'socket'
2
+ require 'geocoder'
2
3
 
3
4
  module Detector
4
5
  class Base
@@ -94,6 +95,221 @@ module Detector
94
95
  nil
95
96
  end
96
97
 
98
+ def geo
99
+ return nil unless valid?
100
+ @geo ||= Geocoder.search(ip).first
101
+ end
102
+
103
+ # Lookup the location for the IP:
104
+ def geography
105
+ return nil unless valid?
106
+ "#{geo.city}, #{geo.region}, #{geo.country}" if geo
107
+ end
108
+
109
+ def region
110
+ return nil unless valid?
111
+
112
+ # Try to determine region from hostname first
113
+ hostname = host.to_s.downcase
114
+
115
+ # AWS regions from hostname
116
+ if hostname =~ /amazonaws\.com/ || hostname =~ /aws/
117
+ return "us-east-1" if hostname =~ /us-east-1|virginia|nova/
118
+ return "us-east-2" if hostname =~ /us-east-2|ohio/
119
+ return "us-west-1" if hostname =~ /us-west-1|california|norcal/
120
+ return "us-west-2" if hostname =~ /us-west-2|oregon/
121
+ return "af-south-1" if hostname =~ /af-south|cape-town/
122
+ return "ap-east-1" if hostname =~ /ap-east|hong-kong/
123
+ return "ap-south-1" if hostname =~ /ap-south|mumbai/
124
+ return "ap-northeast-1" if hostname =~ /ap-northeast-1|tokyo/
125
+ return "ap-northeast-2" if hostname =~ /ap-northeast-2|seoul/
126
+ return "ap-northeast-3" if hostname =~ /ap-northeast-3|osaka/
127
+ return "ap-southeast-1" if hostname =~ /ap-southeast-1|singapore/
128
+ return "ap-southeast-2" if hostname =~ /ap-southeast-2|sydney/
129
+ return "ca-central-1" if hostname =~ /ca-central|canada|montreal/
130
+ return "eu-central-1" if hostname =~ /eu-central-1|frankfurt/
131
+ return "eu-west-1" if hostname =~ /eu-west-1|ireland|dublin/
132
+ return "eu-west-2" if hostname =~ /eu-west-2|london/
133
+ return "eu-west-3" if hostname =~ /eu-west-3|paris/
134
+ return "eu-north-1" if hostname =~ /eu-north-1|stockholm/
135
+ return "eu-south-1" if hostname =~ /eu-south-1|milan/
136
+ return "me-south-1" if hostname =~ /me-south-1|bahrain/
137
+ return "sa-east-1" if hostname =~ /sa-east-1|sao-paulo/
138
+ end
139
+
140
+ # Azure regions from hostname
141
+ if hostname =~ /azure|windows\.net|cloudapp/
142
+ return "eastus" if hostname =~ /eastus|virginia/
143
+ return "eastus2" if hostname =~ /eastus2/
144
+ return "centralus" if hostname =~ /centralus|iowa/
145
+ return "northcentralus" if hostname =~ /northcentralus|illinois/
146
+ return "southcentralus" if hostname =~ /southcentralus|texas/
147
+ return "westus" if hostname =~ /westus|california/
148
+ return "westus2" if hostname =~ /westus2|washington/
149
+ return "westus3" if hostname =~ /westus3|phoenix/
150
+ return "australiaeast" if hostname =~ /australiaeast|sydney/
151
+ return "brazilsouth" if hostname =~ /brazilsouth|sao-paulo/
152
+ return "canadacentral" if hostname =~ /canadacentral|toronto/
153
+ return "centralindia" if hostname =~ /centralindia|pune/
154
+ return "eastasia" if hostname =~ /eastasia|hong-kong/
155
+ return "francecentral" if hostname =~ /francecentral|paris/
156
+ return "germanywestcentral" if hostname =~ /germanywestcentral|frankfurt/
157
+ return "japaneast" if hostname =~ /japaneast|tokyo/
158
+ return "koreacentral" if hostname =~ /koreacentral|seoul/
159
+ return "northeurope" if hostname =~ /northeurope|ireland/
160
+ return "southeastasia" if hostname =~ /southeastasia|singapore/
161
+ return "southindia" if hostname =~ /southindia|chennai/
162
+ return "swedencentral" if hostname =~ /swedencentral|stockholm/
163
+ return "switzerlandnorth" if hostname =~ /switzerlandnorth|zurich/
164
+ return "uksouth" if hostname =~ /uksouth|london/
165
+ return "westeurope" if hostname =~ /westeurope|netherlands/
166
+ end
167
+
168
+ # Google Cloud regions from hostname
169
+ if hostname =~ /google|googlecloud|gcp|appspot/
170
+ return "us-central1" if hostname =~ /us-central1|iowa/
171
+ return "us-east1" if hostname =~ /us-east1|south-carolina/
172
+ return "us-east4" if hostname =~ /us-east4|virginia/
173
+ return "us-west1" if hostname =~ /us-west1|oregon/
174
+ return "us-west2" if hostname =~ /us-west2|los-angeles/
175
+ return "us-west3" if hostname =~ /us-west3|salt-lake-city/
176
+ return "us-west4" if hostname =~ /us-west4|las-vegas/
177
+ return "northamerica-northeast1" if hostname =~ /northamerica-northeast1|montreal/
178
+ return "southamerica-east1" if hostname =~ /southamerica-east1|sao-paulo/
179
+ return "europe-west1" if hostname =~ /europe-west1|belgium/
180
+ return "europe-west2" if hostname =~ /europe-west2|london/
181
+ return "europe-west3" if hostname =~ /europe-west3|frankfurt/
182
+ return "europe-west4" if hostname =~ /europe-west4|netherlands/
183
+ return "europe-west6" if hostname =~ /europe-west6|zurich/
184
+ return "europe-north1" if hostname =~ /europe-north1|finland/
185
+ return "asia-east1" if hostname =~ /asia-east1|taiwan/
186
+ return "asia-east2" if hostname =~ /asia-east2|hong-kong/
187
+ return "asia-northeast1" if hostname =~ /asia-northeast1|tokyo/
188
+ return "asia-northeast2" if hostname =~ /asia-northeast2|osaka/
189
+ return "asia-northeast3" if hostname =~ /asia-northeast3|seoul/
190
+ return "asia-south1" if hostname =~ /asia-south1|mumbai/
191
+ return "asia-southeast1" if hostname =~ /asia-southeast1|singapore/
192
+ return "asia-southeast2" if hostname =~ /asia-southeast2|jakarta/
193
+ return "australia-southeast1" if hostname =~ /australia-southeast1|sydney/
194
+ end
195
+
196
+ # Fallback to IP-based lookup via Geocoder
197
+ return nil unless geo
198
+
199
+ # City-based detection for common cloud cities
200
+ city = geo&.data&.dig('city')&.downcase
201
+ if city
202
+ case city
203
+ when 'ashburn', 'sterling', 'herndon', 'chantilly'
204
+ return "us-east-1" # AWS us-east-1 or equivalent
205
+ when 'columbus', 'dublin', 'hilliard'
206
+ return "us-east-2" # AWS us-east-2
207
+ when 'san jose', 'santa clara', 'milpitas', 'fremont'
208
+ return "us-west-1" # AWS us-west-1
209
+ when 'portland', 'hillsboro', 'prineville', 'the dalles'
210
+ return "us-west-2" # AWS us-west-2 / GCP us-west1
211
+ when 'phoenix', 'tempe', 'mesa'
212
+ return "westus3" # Azure westus3
213
+ when 'dallas', 'fort worth', 'san antonio'
214
+ return "southcentralus" # Azure southcentralus
215
+ when 'montreal', 'beauharnois', 'quebec'
216
+ return "ca-central-1" # AWS ca-central-1
217
+ when 'toronto'
218
+ return "canadacentral" # Azure canadacentral
219
+ when 'frankfurt', 'munich'
220
+ return "eu-central-1" # AWS eu-central-1
221
+ when 'london'
222
+ return "eu-west-2" # AWS eu-west-2
223
+ when 'paris'
224
+ return "eu-west-3" # AWS eu-west-3
225
+ when 'dublin', 'clondalkin'
226
+ return "eu-west-1" # AWS eu-west-1
227
+ when 'stockholm'
228
+ return "eu-north-1" # AWS eu-north-1
229
+ when 'milan'
230
+ return "eu-south-1" # AWS eu-south-1
231
+ when 'sydney', 'melbourne'
232
+ return "ap-southeast-2" # AWS ap-southeast-2
233
+ when 'singapore'
234
+ return "ap-southeast-1" # AWS ap-southeast-1
235
+ when 'tokyo', 'osaka'
236
+ return "ap-northeast-1" # AWS ap-northeast-1
237
+ when 'seoul'
238
+ return "ap-northeast-2" # AWS ap-northeast-2
239
+ when 'mumbai'
240
+ return "ap-south-1" # AWS ap-south-1
241
+ when 'hong kong'
242
+ return "ap-east-1" # AWS ap-east-1
243
+ when 'são paulo', 'sao paulo'
244
+ return "sa-east-1" # AWS sa-east-1
245
+ end
246
+ end
247
+
248
+ # Region/State-based mapping to approximate cloud region
249
+ region_name = geo&.data&.dig('region')
250
+ country = geo&.data&.dig('country')
251
+
252
+ if country == 'United States'
253
+ case region_name
254
+ when 'Virginia', 'Maryland', 'District of Columbia'
255
+ return "us-east-1"
256
+ when 'Ohio', 'Indiana', 'Michigan'
257
+ return "us-east-2"
258
+ when 'California'
259
+ return "us-west-1"
260
+ when 'Oregon', 'Washington', 'Idaho'
261
+ return "us-west-2"
262
+ when 'Nevada', 'Utah', 'Arizona'
263
+ return "us-west-2"
264
+ when 'Texas', 'Oklahoma', 'Louisiana'
265
+ return "southcentralus"
266
+ when 'Illinois', 'Iowa', 'Minnesota', 'Missouri', 'Wisconsin'
267
+ return "us-central1"
268
+ end
269
+ elsif country == 'Canada'
270
+ case region_name
271
+ when 'Quebec'
272
+ return "ca-central-1"
273
+ when 'Ontario'
274
+ return "canadacentral"
275
+ end
276
+ elsif country == 'Brazil'
277
+ return "sa-east-1"
278
+ elsif country == 'Ireland'
279
+ return "eu-west-1"
280
+ elsif country == 'United Kingdom'
281
+ return "eu-west-2"
282
+ elsif country == 'France'
283
+ return "eu-west-3"
284
+ elsif country == 'Germany'
285
+ return "eu-central-1"
286
+ elsif country == 'Sweden' || country == 'Norway' || country == 'Finland'
287
+ return "eu-north-1"
288
+ elsif country == 'Italy'
289
+ return "eu-south-1"
290
+ elsif country == 'India'
291
+ return "ap-south-1"
292
+ elsif country == 'Singapore'
293
+ return "ap-southeast-1"
294
+ elsif country == 'Australia'
295
+ return "ap-southeast-2"
296
+ elsif country == 'Japan'
297
+ return "ap-northeast-1"
298
+ elsif country == 'South Korea'
299
+ return "ap-northeast-2"
300
+ elsif country == 'Hong Kong'
301
+ return "ap-east-1"
302
+ end
303
+
304
+ # Final fallback
305
+ region_name || geo&.data&.dig('country_code')&.downcase
306
+ end
307
+
308
+ def asn
309
+ return nil unless valid?
310
+ geo&.data&.dig('org')&.split(" ")&.first
311
+ end
312
+
97
313
  def connection?
98
314
  connection.present?
99
315
  end
@@ -138,5 +354,87 @@ module Detector
138
354
  def usage
139
355
  nil
140
356
  end
357
+
358
+ def user_access_level
359
+ return nil unless valid? && connection?
360
+ "Unknown"
361
+ end
362
+
363
+ def infrastructure
364
+ return nil unless valid?
365
+
366
+ hostname = host.to_s.downcase
367
+ case hostname
368
+ when /amazon/, /aws/, /amazonaws/, /ec2/, /s3/, /dynamodb/, /rds\./, /elasticbeanstalk/
369
+ "Amazon Web Services"
370
+ when /google/, /googlecloud/, /appspot/, /gcp/, /compute\./, /cloud\.g/
371
+ "Google Cloud Platform"
372
+ when /azure/, /azurewebsites/, /cloudapp\./, /windows\.net/
373
+ "Microsoft Azure"
374
+ when /antimony/
375
+ "Build.io"
376
+ when /heroku/, /herokuapp/
377
+ "Heroku"
378
+ when /digitalocean/, /droplet/
379
+ "DigitalOcean"
380
+ when /linode/, /linodeobjects/
381
+ "Linode"
382
+ when /vultr/
383
+ "Vultr"
384
+ when /netlify/
385
+ "Netlify"
386
+ when /vercel/, /zeit\.co/, /now\.sh/
387
+ "Vercel"
388
+ when /github\.io/, /githubusercontent/, /github\.dev/
389
+ "GitHub"
390
+ when /gitlab\.io/, /gitlab-static/
391
+ "GitLab"
392
+ when /oracle/, /oraclecloud/
393
+ "Oracle Cloud"
394
+ when /ibm/, /bluemix/, /ibmcloud/
395
+ "IBM Cloud"
396
+ when /cloudflare/, /workers\.dev/
397
+ "Cloudflare"
398
+ when /fastly/
399
+ "Fastly"
400
+ when /akamai/
401
+ "Akamai"
402
+ when /render\.com/
403
+ "Render"
404
+ when /fly\.io/
405
+ "Fly.io"
406
+ when /railway\.app/
407
+ "Railway"
408
+ when /upcloud/
409
+ "UpCloud"
410
+ when /hetzner/
411
+ "Hetzner"
412
+ when /ovh/, /ovhcloud/
413
+ "OVH"
414
+ when /scaleway/
415
+ "Scaleway"
416
+ when /contabo/
417
+ "Contabo"
418
+ when /dreamhost/
419
+ "DreamHost"
420
+ when /hostgator/
421
+ "HostGator"
422
+ when /bluehost/
423
+ "Bluehost"
424
+ when /siteground/
425
+ "SiteGround"
426
+ when /namecheap/
427
+ "Namecheap"
428
+ when /godaddy/
429
+ "GoDaddy"
430
+ when /ionos/
431
+ "IONOS"
432
+ when /hostinger/
433
+ "Hostinger"
434
+ else
435
+ # If geo data available, return organization, otherwise nil
436
+ geo&.data&.dig('org')
437
+ end
438
+ end
141
439
  end
142
440
  end
@@ -1,3 +1,3 @@
1
1
  module Detector
2
- VERSION = "0.1.1"
2
+ VERSION = "0.2.1"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: detector
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jonathan Siegel
@@ -108,6 +108,48 @@ dependencies:
108
108
  - - "~>"
109
109
  - !ruby/object:Gem::Version
110
110
  version: 0.3.3
111
+ - !ruby/object:Gem::Dependency
112
+ name: geocoder
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '1.8'
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '1.8'
125
+ - !ruby/object:Gem::Dependency
126
+ name: logger
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '1.5'
132
+ type: :runtime
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '1.5'
139
+ - !ruby/object:Gem::Dependency
140
+ name: ostruct
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: '0.5'
146
+ type: :runtime
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: '0.5'
111
153
  - !ruby/object:Gem::Dependency
112
154
  name: rspec
113
155
  requirement: !ruby/object:Gem::Requirement