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 +4 -4
- data/README.md +8 -0
- data/bin/detector +22 -0
- data/lib/detector/addons/mariadb.rb +26 -0
- data/lib/detector/addons/mysql.rb +61 -0
- data/lib/detector/addons/postgres.rb +40 -0
- data/lib/detector/addons/redis.rb +76 -0
- data/lib/detector/addons/smtp.rb +22 -0
- data/lib/detector/base.rb +298 -0
- data/lib/detector/version.rb +1 -1
- metadata +43 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ab9800bbb6b9322ca7e8115b438c89cf5da96febc891e1ae6cf55ff8de6ff011
|
4
|
+
data.tar.gz: 4916567db9f219e16d9fe3bef938dbc51958d786205894a850074686c5e41400
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
|
data/lib/detector/addons/smtp.rb
CHANGED
@@ -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
|
data/lib/detector/version.rb
CHANGED
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.
|
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
|