lanet 0.1.0 → 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/CHANGELOG.md +39 -21
- data/Gemfile.lock +1 -1
- data/README.md +86 -2
- data/index.html +424 -223
- data/lib/lanet/cli.rb +110 -62
- data/lib/lanet/encryptor.rb +94 -16
- data/lib/lanet/scanner.rb +101 -135
- data/lib/lanet/signer.rb +47 -0
- data/lib/lanet/version.rb +1 -1
- metadata +3 -2
data/lib/lanet/scanner.rb
CHANGED
@@ -25,55 +25,54 @@ module Lanet
|
|
25
25
|
8443 => "HTTPS-ALT"
|
26
26
|
}.freeze
|
27
27
|
|
28
|
-
# Ports to check during scan
|
29
28
|
QUICK_CHECK_PORTS = [80, 443, 22, 445, 139, 8080].freeze
|
30
29
|
|
31
30
|
def initialize
|
32
31
|
@hosts = []
|
33
32
|
@mutex = Mutex.new
|
33
|
+
@arp_cache = {}
|
34
34
|
end
|
35
35
|
|
36
|
-
# Scan network and return active hosts
|
37
36
|
def scan(cidr, timeout = 1, max_threads = 32, verbose = false)
|
38
37
|
@verbose = verbose
|
39
38
|
@timeout = timeout
|
40
|
-
|
41
|
-
# Clear previous scan results
|
42
39
|
@hosts = []
|
43
|
-
|
44
|
-
# Get the range of IP addresses to scan
|
45
40
|
range = IPAddr.new(cidr).to_range
|
46
|
-
|
47
|
-
# Create a queue of IPs to scan
|
48
41
|
queue = Queue.new
|
49
42
|
range.each { |ip| queue << ip.to_s }
|
50
|
-
|
51
43
|
total_ips = queue.size
|
52
44
|
completed = 0
|
53
45
|
|
54
|
-
#
|
55
|
-
|
46
|
+
# Initial ARP cache population
|
47
|
+
@arp_cache = parse_arp_table
|
56
48
|
|
57
|
-
# Create worker threads to process the queue
|
58
49
|
threads = Array.new([max_threads, total_ips].min) do
|
59
50
|
Thread.new do
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
51
|
+
loop do
|
52
|
+
begin
|
53
|
+
ip = queue.pop(true)
|
54
|
+
rescue ThreadError
|
55
|
+
break
|
56
|
+
end
|
65
57
|
scan_host(ip)
|
66
58
|
@mutex.synchronize do
|
67
59
|
completed += 1
|
68
60
|
if total_ips < 100 || (completed % 10).zero? || completed == total_ips
|
69
|
-
print_progress(completed,
|
70
|
-
total_ips)
|
61
|
+
print_progress(completed, total_ips)
|
71
62
|
end
|
72
63
|
end
|
73
64
|
end
|
74
65
|
end
|
75
66
|
end
|
76
67
|
|
68
|
+
# Periodically update ARP cache
|
69
|
+
arp_updater = Thread.new do
|
70
|
+
while threads.any?(&:alive?)
|
71
|
+
sleep 5
|
72
|
+
@mutex.synchronize { @arp_cache = parse_arp_table }
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
77
76
|
begin
|
78
77
|
threads.each(&:join)
|
79
78
|
print_progress(total_ips, total_ips)
|
@@ -82,6 +81,8 @@ module Lanet
|
|
82
81
|
rescue Interrupt
|
83
82
|
puts "\nScan interrupted. Returning partial results..."
|
84
83
|
@verbose ? @hosts : @hosts.map { |h| h[:ip] }
|
84
|
+
ensure
|
85
|
+
arp_updater.kill if arp_updater.alive?
|
85
86
|
end
|
86
87
|
end
|
87
88
|
|
@@ -92,47 +93,62 @@ module Lanet
|
|
92
93
|
print "\rScanning network: #{percent}% complete (#{completed}/#{total})"
|
93
94
|
end
|
94
95
|
|
95
|
-
def
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
range.each { |ip| queue << ip.to_s }
|
96
|
+
def parse_arp_table
|
97
|
+
cmd = RbConfig::CONFIG["host_os"] =~ /mswin|mingw|cygwin/ ? "arp -a" : "arp -a"
|
98
|
+
output = `#{cmd}`
|
99
|
+
arp_cache = {}
|
100
100
|
|
101
|
-
|
102
|
-
|
101
|
+
case RbConfig::CONFIG["host_os"]
|
102
|
+
when /darwin/
|
103
|
+
output.each_line do |line|
|
104
|
+
next unless line =~ /\((\d+\.\d+\.\d+\.\d+)\) at ([0-9a-f:]+) on/
|
103
105
|
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
queue.pop(true)
|
108
|
-
rescue ThreadError
|
109
|
-
nil
|
110
|
-
end)
|
111
|
-
# Use system ping to update ARP table
|
112
|
-
system("ping -c 1 -W 1 #{ip} > /dev/null 2>&1 &")
|
113
|
-
sleep 0.01 # Small delay to prevent overwhelming the system
|
114
|
-
processed += 1
|
115
|
-
end
|
106
|
+
ip = ::Regexp.last_match(1)
|
107
|
+
mac = ::Regexp.last_match(2).downcase
|
108
|
+
arp_cache[ip] = mac unless mac == "(incomplete)"
|
116
109
|
end
|
117
|
-
|
110
|
+
when /linux/
|
111
|
+
output.each_line do |line|
|
112
|
+
next unless line =~ /^(\d+\.\d+\.\d+\.\d+)\s+\w+\s+([0-9a-f:]+)\s+/
|
113
|
+
|
114
|
+
ip = ::Regexp.last_match(1)
|
115
|
+
mac = ::Regexp.last_match(2).downcase
|
116
|
+
arp_cache[ip] = mac unless mac == "00:00:00:00:00:00"
|
117
|
+
end
|
118
|
+
when /mswin|mingw|cygwin/
|
119
|
+
output.each_line do |line|
|
120
|
+
next unless line =~ /^\s*(\d+\.\d+\.\d+\.\d+)\s+([0-9a-f-]+)\s+/
|
118
121
|
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
+
ip = ::Regexp.last_match(1)
|
123
|
+
mac = ::Regexp.last_match(2).gsub("-", ":").downcase
|
124
|
+
arp_cache[ip] = mac
|
125
|
+
end
|
126
|
+
end
|
127
|
+
arp_cache
|
122
128
|
end
|
123
129
|
|
124
130
|
def scan_host(ip)
|
125
|
-
#
|
126
|
-
|
131
|
+
# Handle broadcast addresses immediately
|
132
|
+
if ip.end_with?(".255") || ip == "255.255.255.255"
|
133
|
+
host_info = { ip: ip, mac: "ff:ff:ff:ff:ff:ff", response_time: 0, detection_method: "Broadcast" }
|
134
|
+
if @verbose
|
135
|
+
host_info[:hostname] = "Broadcast"
|
136
|
+
host_info[:ports] = {}
|
137
|
+
end
|
138
|
+
@mutex.synchronize { @hosts << host_info }
|
139
|
+
return
|
140
|
+
end
|
141
|
+
|
142
|
+
# Skip network addresses
|
143
|
+
return if ip.end_with?(".0") && !ip.end_with?(".0.0")
|
127
144
|
|
128
|
-
# Use multiple methods to detect if a host is alive
|
129
145
|
is_active = false
|
130
146
|
detection_method = nil
|
131
147
|
response_time = nil
|
132
148
|
start_time = Time.now
|
133
149
|
open_ports = []
|
134
150
|
|
135
|
-
#
|
151
|
+
# TCP port scan
|
136
152
|
tcp_result = tcp_port_scan(ip, QUICK_CHECK_PORTS)
|
137
153
|
if tcp_result[:active]
|
138
154
|
is_active = true
|
@@ -140,116 +156,86 @@ module Lanet
|
|
140
156
|
open_ports = tcp_result[:open_ports]
|
141
157
|
end
|
142
158
|
|
143
|
-
#
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
is_active = true
|
148
|
-
detection_method = "ICMP"
|
149
|
-
end
|
159
|
+
# ICMP ping
|
160
|
+
if !is_active && ping_check(ip)
|
161
|
+
is_active = true
|
162
|
+
detection_method = "ICMP"
|
150
163
|
end
|
151
164
|
|
152
|
-
#
|
153
|
-
if !is_active && (ip.end_with?(".1") || ip.end_with?(".254")
|
154
|
-
|
155
|
-
|
156
|
-
is_active = true
|
157
|
-
detection_method = "UDP"
|
158
|
-
end
|
165
|
+
# UDP check for common network devices
|
166
|
+
if !is_active && (ip.end_with?(".1") || ip.end_with?(".254")) && udp_check(ip)
|
167
|
+
is_active = true
|
168
|
+
detection_method = "UDP"
|
159
169
|
end
|
160
170
|
|
161
|
-
#
|
171
|
+
# ARP check
|
162
172
|
unless is_active
|
163
|
-
|
164
|
-
if
|
173
|
+
mac = get_mac_address(ip)
|
174
|
+
if mac && mac != "(incomplete)"
|
165
175
|
is_active = true
|
166
176
|
detection_method = "ARP"
|
167
177
|
end
|
168
178
|
end
|
169
179
|
|
170
|
-
# For broadcast addresses, always consider them active
|
171
|
-
if ip.end_with?(".255") || ip == "255.255.255.255"
|
172
|
-
is_active = true
|
173
|
-
detection_method = "Broadcast"
|
174
|
-
end
|
175
|
-
|
176
|
-
# Calculate response time
|
177
180
|
response_time = ((Time.now - start_time) * 1000).round(2) if is_active
|
178
|
-
|
179
181
|
return unless is_active
|
180
182
|
|
181
|
-
|
182
|
-
host_info = {
|
183
|
-
ip: ip,
|
184
|
-
mac: get_mac_address(ip),
|
185
|
-
response_time: response_time,
|
186
|
-
detection_method: detection_method
|
187
|
-
}
|
183
|
+
host_info = { ip: ip, mac: get_mac_address(ip), response_time: response_time, detection_method: detection_method }
|
188
184
|
|
189
185
|
if @verbose
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
end
|
195
|
-
rescue Resolv::ResolvError, Timeout::Error
|
196
|
-
host_info[:hostname] = "Unknown"
|
186
|
+
host_info[:hostname] = begin
|
187
|
+
Timeout.timeout(1) { Resolv.getname(ip) }
|
188
|
+
rescue StandardError
|
189
|
+
"Unknown"
|
197
190
|
end
|
198
|
-
|
199
|
-
# For verbose mode, scan more ports if TCP detection method was successful
|
200
191
|
if detection_method == "TCP"
|
201
192
|
extra_ports = tcp_port_scan(ip, COMMON_PORTS.keys - QUICK_CHECK_PORTS)[:open_ports]
|
202
193
|
open_ports += extra_ports
|
203
194
|
end
|
204
|
-
|
205
195
|
host_info[:ports] = open_ports.map { |port| [port, COMMON_PORTS[port] || "Unknown"] }.to_h
|
206
196
|
end
|
207
197
|
|
208
198
|
@mutex.synchronize { @hosts << host_info }
|
209
|
-
rescue StandardError => e
|
210
|
-
puts "\nError scanning host #{ip}: #{e.message}" if $DEBUG
|
211
199
|
end
|
212
200
|
|
213
201
|
def tcp_port_scan(ip, ports)
|
214
202
|
open_ports = []
|
215
203
|
is_active = false
|
204
|
+
threads = ports.map do |port|
|
205
|
+
Thread.new(port) do |p|
|
206
|
+
Timeout.timeout(@timeout) do
|
207
|
+
socket = TCPSocket.new(ip, p)
|
208
|
+
Thread.current[:open] = p
|
209
|
+
socket.close
|
210
|
+
end
|
211
|
+
rescue Errno::ECONNREFUSED
|
212
|
+
Thread.current[:active] = true
|
213
|
+
rescue StandardError
|
214
|
+
# Port closed or filtered
|
215
|
+
end
|
216
|
+
end
|
216
217
|
|
217
|
-
|
218
|
-
|
219
|
-
|
218
|
+
threads.each do |thread|
|
219
|
+
thread.join
|
220
|
+
if thread[:open]
|
221
|
+
open_ports << thread[:open]
|
222
|
+
is_active = true
|
223
|
+
elsif thread[:active]
|
220
224
|
is_active = true
|
221
|
-
open_ports << port
|
222
|
-
socket.close
|
223
225
|
end
|
224
|
-
rescue Errno::ECONNREFUSED
|
225
|
-
# Connection refused means host is up but port is closed
|
226
|
-
is_active = true
|
227
|
-
rescue StandardError
|
228
|
-
# Other errors mean port is probably closed or filtered
|
229
226
|
end
|
230
227
|
|
231
228
|
{ active: is_active, open_ports: open_ports }
|
232
229
|
end
|
233
230
|
|
234
231
|
def ping_check(ip)
|
235
|
-
cmd =
|
236
|
-
when /darwin/
|
237
|
-
"ping -c 1 -W 1 #{ip}"
|
238
|
-
when /linux/
|
239
|
-
"ping -c 1 -W 1 #{ip}"
|
240
|
-
when /mswin|mingw|cygwin/
|
241
|
-
"ping -n 1 -w 1000 #{ip}"
|
242
|
-
else
|
243
|
-
"ping -c 1 -W 1 #{ip}"
|
244
|
-
end
|
245
|
-
|
232
|
+
cmd = RbConfig::CONFIG["host_os"] =~ /mswin|mingw|cygwin/ ? "ping -n 1 -w 1000 #{ip}" : "ping -c 1 -W 1 #{ip}"
|
246
233
|
system("#{cmd} > /dev/null 2>&1")
|
247
234
|
$CHILD_STATUS.exitstatus.zero?
|
248
235
|
end
|
249
236
|
|
250
237
|
def udp_check(ip)
|
251
238
|
common_udp_ports = [53, 67, 68, 123, 137, 138, 1900, 5353]
|
252
|
-
|
253
239
|
common_udp_ports.each do |port|
|
254
240
|
Timeout.timeout(0.5) do
|
255
241
|
socket = UDPSocket.new
|
@@ -259,35 +245,15 @@ module Lanet
|
|
259
245
|
return true
|
260
246
|
end
|
261
247
|
rescue Errno::ECONNREFUSED
|
262
|
-
return true
|
248
|
+
return true
|
263
249
|
rescue StandardError
|
264
|
-
|
250
|
+
next
|
265
251
|
end
|
266
252
|
false
|
267
253
|
end
|
268
254
|
|
269
255
|
def get_mac_address(ip)
|
270
|
-
|
271
|
-
|
272
|
-
# Get MAC from ARP table
|
273
|
-
cmd = case RbConfig::CONFIG["host_os"]
|
274
|
-
when /darwin/
|
275
|
-
"arp -n #{ip}"
|
276
|
-
when /linux/
|
277
|
-
"arp -n #{ip}"
|
278
|
-
when /mswin|mingw|cygwin/
|
279
|
-
"arp -a #{ip}"
|
280
|
-
else
|
281
|
-
"arp -n #{ip}"
|
282
|
-
end
|
283
|
-
|
284
|
-
output = `#{cmd}`
|
285
|
-
|
286
|
-
if output =~ /([0-9a-fA-F]{1,2}[:-][0-9a-fA-F]{1,2}[:-][0-9a-fA-F]{1,2}[:-][0-9a-fA-F]{1,2}[:-][0-9a-fA-F]{1,2}[:-][0-9a-fA-F]{1,2})/
|
287
|
-
::Regexp.last_match(1).downcase
|
288
|
-
else
|
289
|
-
"(incomplete)"
|
290
|
-
end
|
256
|
+
@mutex.synchronize { @arp_cache[ip] || "(incomplete)" }
|
291
257
|
end
|
292
258
|
end
|
293
259
|
end
|
data/lib/lanet/signer.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "openssl"
|
4
|
+
require "base64"
|
5
|
+
|
6
|
+
module Lanet
|
7
|
+
class Signer
|
8
|
+
# Error class for signature failures
|
9
|
+
class Error < StandardError; end
|
10
|
+
|
11
|
+
# Signs a message using the provided private key
|
12
|
+
# @param message [String] the message to sign
|
13
|
+
# @param private_key_pem [String] the PEM-encoded private key
|
14
|
+
# @return [String] Base64-encoded signature
|
15
|
+
def self.sign(message, private_key_pem)
|
16
|
+
private_key = OpenSSL::PKey::RSA.new(private_key_pem)
|
17
|
+
signature = private_key.sign(OpenSSL::Digest.new("SHA256"), message.to_s)
|
18
|
+
Base64.strict_encode64(signature)
|
19
|
+
rescue StandardError => e
|
20
|
+
raise Error, "Signing failed: #{e.message}"
|
21
|
+
end
|
22
|
+
|
23
|
+
# Verifies a signature using the provided public key
|
24
|
+
# @param message [String] the original message
|
25
|
+
# @param signature_base64 [String] the Base64-encoded signature
|
26
|
+
# @param public_key_pem [String] the PEM-encoded public key
|
27
|
+
# @return [Boolean] true if signature is valid
|
28
|
+
def self.verify(message, signature_base64, public_key_pem)
|
29
|
+
public_key = OpenSSL::PKey::RSA.new(public_key_pem)
|
30
|
+
signature = Base64.strict_decode64(signature_base64)
|
31
|
+
public_key.verify(OpenSSL::Digest.new("SHA256"), signature, message.to_s)
|
32
|
+
rescue StandardError => e
|
33
|
+
raise Error, "Verification failed: #{e.message}"
|
34
|
+
end
|
35
|
+
|
36
|
+
# Generates a new RSA key pair
|
37
|
+
# @param bits [Integer] key size in bits
|
38
|
+
# @return [Hash] containing :private_key and :public_key as PEM strings
|
39
|
+
def self.generate_key_pair(bits = 2048)
|
40
|
+
key = OpenSSL::PKey::RSA.new(bits)
|
41
|
+
{
|
42
|
+
private_key: key.to_pem,
|
43
|
+
public_key: key.public_key.to_pem
|
44
|
+
}
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
data/lib/lanet/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: lanet
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1
|
4
|
+
version: 0.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Davide Santangelo
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-03-
|
11
|
+
date: 2025-03-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: thor
|
@@ -97,6 +97,7 @@ files:
|
|
97
97
|
- lib/lanet/receiver.rb
|
98
98
|
- lib/lanet/scanner.rb
|
99
99
|
- lib/lanet/sender.rb
|
100
|
+
- lib/lanet/signer.rb
|
100
101
|
- lib/lanet/version.rb
|
101
102
|
- sig/lanet.rbs
|
102
103
|
homepage: https://github.com/davidesantangelo/lanet
|