netmap-scanner 1.0.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 +7 -0
- data/LICENSE +21 -0
- data/README.md +116 -0
- data/assets/logo.png +0 -0
- data/bin/netmap +13 -0
- data/lib/netmap/cli.rb +261 -0
- data/lib/netmap/html_reporter.rb +78 -0
- data/lib/netmap/scanner.rb +472 -0
- data/lib/netmap/script_engine.rb +53 -0
- data/lib/netmap/version.rb +3 -0
- data/lib/netmap.rb +9 -0
- data/scripts/http_check.rb +9 -0
- metadata +97 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 96dfd02daaa9dfe887fbf0c693d8dd302a6f64eff4c3ebdc759744e5de3c31cb
|
|
4
|
+
data.tar.gz: 5aa4fbd4522755cba2e0ceacde99232e93fe7ae188bea17bbc8b84ddfb4f2649
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 4d3f9aa90256ac2331f8a2e9de9efa9ab7e59f0c05a9fa7094a4ff72b2d173b6699f037d235fc3d857752ea26094e5c82585aef3b68aea2e318a19c1ee174d21
|
|
7
|
+
data.tar.gz: e586f72b5787d851dd749dcc21b1c8be0f2af68939eb93552421aada7c4561d1a71c72b0440af7d252ed0d84d531b66caf8a363a2c119a033c9d8a95b418a93e
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 IshikawaUta
|
|
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,116 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="assets/logo.png" alt="NetMap Ultimate Logo" width="600">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
# 🛰️ NetMap Ultimate V1.0
|
|
6
|
+
|
|
7
|
+
[](https://www.ruby-lang.org/)
|
|
8
|
+
[](LICENSE)
|
|
9
|
+
[]()
|
|
10
|
+
|
|
11
|
+
**NetMap Ultimate** adalah alat pemindai jaringan (Network Scanner) berperforma tinggi yang ditulis dalam bahasa Ruby. Dirancang untuk kecepatan dan akurasi, NetMap mampu memetakan seluruh jaringan lokal atau WiFi Anda dalam hitungan detik menggunakan teknologi *Asynchronous I/O*.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## ✨ Fitur Utama
|
|
16
|
+
|
|
17
|
+
### 📡 Ultimate Host Discovery
|
|
18
|
+
- **Multi-Layer Detection**: Menggabungkan ICMP Ping, ARP Cache Lookup (L2), dan TCP Ping Fallback.
|
|
19
|
+
- **ARP Precision**: Deteksi instan perangkat di jaringan lokal dengan membaca tabel ARP sistem secara langsung.
|
|
20
|
+
- **Smart Pn Mode**: Lewati *Host Discovery* dengan flag `-Pn` untuk memindai target yang memblokir semua jenis ping.
|
|
21
|
+
|
|
22
|
+
### 🏢 Identifikasi & Fingerprinting
|
|
23
|
+
- **OUI Vendor Mapping**: Mengenali merk perangkat (Apple, Cisco, VMware, dll) berdasarkan prefix MAC Address.
|
|
24
|
+
- **OS Prediction**: Tebakan Sistem Operasi (Linux/Unix, Windows, Cisco) menggunakan analisis TTL (*Time To Live*).
|
|
25
|
+
- **Service Grabbing**: Banner grabbing otomatis untuk SSH, MySQL, HTTP, dan layanan populer lainnya.
|
|
26
|
+
|
|
27
|
+
### ⚡ Performa Tinggi & Kustomisasi
|
|
28
|
+
- **Async Concurrency Engine**: Menangani ratusan tugas pemindaian secara paralel tanpa membebani memori.
|
|
29
|
+
- **Flexible Timing**: 6 template kecepatan dari **Paranoid** hingga **Insane** untuk menyesuaikan diri dengan sensitivitas jaringan target.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## 🚀 Instalasi
|
|
34
|
+
|
|
35
|
+
### Prasyarat
|
|
36
|
+
- **Ruby** >= 3.0
|
|
37
|
+
- **Linux** (Direkomendasikan untuk fitur pemindaian Layer 2 / ARP terbaik)
|
|
38
|
+
|
|
39
|
+
### Cara Instalasi
|
|
40
|
+
```bash
|
|
41
|
+
git clone https://github.com/IshikawaUta/netmap-scanner.git
|
|
42
|
+
cd netmap-scanner
|
|
43
|
+
bundle install
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## 📖 Referensi Perintah (CLI Options)
|
|
49
|
+
|
|
50
|
+
| Opsi | Deskripsi |
|
|
51
|
+
| :--- | :--- |
|
|
52
|
+
| `-p <range>` | Tentukan port (misal: `80,443` atau `1-1024`). |
|
|
53
|
+
| `-sn` | *Ping scan* saja (hanya mencari host yang aktif). |
|
|
54
|
+
| `-Pn` | Lewati *Host Discovery* (anggap host aktif). |
|
|
55
|
+
| `-O` | Aktifkan deteksi Sistem Operasi (OS Detection). |
|
|
56
|
+
| `-sU` | Aktifkan pemindaian berbasis UDP. |
|
|
57
|
+
| `-T <0-5>` | Pilih *Timing Template* (0: Lambat, 5: Sangat Cepat). |
|
|
58
|
+
| `-t <n>` | Atur jumlah *concurrency thread* secara manual. |
|
|
59
|
+
| `-j <file>` | Simpan hasil ke format JSON. |
|
|
60
|
+
| `--html <file>` | Simpan hasil ke laporan HTML yang cantik. |
|
|
61
|
+
| `-g <file>` | Simpan hasil ke format `grepable` (Nmap style). |
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## 🕒 Timing Templates (-T)
|
|
66
|
+
|
|
67
|
+
| Template | Nama | Concurrency | Timeout | Karakteristik |
|
|
68
|
+
| :---: | :--- | :---: | :---: | :--- |
|
|
69
|
+
| **0** | Paranoid | 1 | 10.0s | Sangat lambat, untuk melewati IDS/Firewall. |
|
|
70
|
+
| **1** | Sneaky | 5 | 5.0s | Pelan dan hati-hati. |
|
|
71
|
+
| **2** | Polite | 10 | 2.0s | Mengurangi beban pada jaringan/target. |
|
|
72
|
+
| **3** | Normal | 50 | 1.0s | Keseimbangan antara kecepatan dan akurasi (Default). |
|
|
73
|
+
| **4** | Aggressive | 200 | 0.8s | Sangat cepat, cocok untuk jaringan yang stabil. |
|
|
74
|
+
| **5** | Insane | 400 | 0.5s | Sangat agresif, butuh *bandwidth* yang besar. |
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## 🌀 Scripting Engine (Kustom Audit)
|
|
79
|
+
|
|
80
|
+
NetMap menyertakan *engine* skrip Ruby yang ringan untuk memperluas kemampuan audit. Anda bisa menambahkan skrip audit sendiri di folder `scripts/`.
|
|
81
|
+
|
|
82
|
+
**Contoh Skrip (`scripts/my_check.rb`):**
|
|
83
|
+
```ruby
|
|
84
|
+
# Skrip sederhana untuk mendeteksi banner tertentu
|
|
85
|
+
if banner.include?("Apache")
|
|
86
|
+
"Apache Web Server Terdeteksi"
|
|
87
|
+
elsif port == 8080
|
|
88
|
+
"Port Proxy Terdeteksi"
|
|
89
|
+
else
|
|
90
|
+
nil
|
|
91
|
+
end
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**Variabel yang tersedia di dalam skrip:**
|
|
95
|
+
- `ip`: Alamat IP target.
|
|
96
|
+
- `port`: Nomor port yang sedang dipindai.
|
|
97
|
+
- `banner`: Banner layanan yang didapat.
|
|
98
|
+
- `os`: Nama Sistem Operasi (jika `-O` aktif).
|
|
99
|
+
- `type`: Jenis protokol (`TCP` atau `UDP`).
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## 🔧 Persyaratan Sistem & Troubleshooting
|
|
104
|
+
|
|
105
|
+
- **Privilese Sudo**: Fitur deteksi MAC Address pada jaringan lokal seringkali membutuhkan akses `sudo` atau hak akses tinggi untuk membaca tabel ARP sistem.
|
|
106
|
+
- **Host Discovery**: Jika target berada di balik *firewall* ketat, gunakan `-Pn` untuk memaksakan pemindaian port.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## 🤝 Kontribusi
|
|
111
|
+
|
|
112
|
+
Kontribusi selalu diterima! Rasakan kebebasan untuk mengirimkan Pull Request atau melaporkan bug melalui Issue di GitHub.
|
|
113
|
+
|
|
114
|
+
## ⚖️ Lisensi
|
|
115
|
+
|
|
116
|
+
Proyek ini dilisensikan di bawah **MIT License**. Lihat file `LICENSE` untuk informasi lebih lanjut.
|
data/assets/logo.png
ADDED
|
Binary file
|
data/bin/netmap
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
require 'bundler/setup'
|
|
5
|
+
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
|
|
6
|
+
require 'netmap'
|
|
7
|
+
|
|
8
|
+
begin
|
|
9
|
+
NetMap::CLI.start(ARGV)
|
|
10
|
+
rescue Interrupt
|
|
11
|
+
puts "\nDihentikan oleh pengguna.".yellow
|
|
12
|
+
exit 0
|
|
13
|
+
end
|
data/lib/netmap/cli.rb
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
require 'optparse'
|
|
2
|
+
require 'colorize'
|
|
3
|
+
require_relative 'scanner'
|
|
4
|
+
|
|
5
|
+
module NetMap
|
|
6
|
+
class CLI
|
|
7
|
+
BANNER = <<-'EOF'.cyan
|
|
8
|
+
_ _ _ __ __
|
|
9
|
+
| \ | | ___| |_| \/ | __ _ _ __
|
|
10
|
+
| \| |/ _ \ __| |\/| |/ _` | '_ \
|
|
11
|
+
| |\ | __/ |_| | | | (_| | |_) |
|
|
12
|
+
|_| \_|\___|\__|_| |_|\__,_| .__/
|
|
13
|
+
|_|
|
|
14
|
+
[ Network Scanner - By: IshikawaUta ]
|
|
15
|
+
EOF
|
|
16
|
+
|
|
17
|
+
def self.start(args)
|
|
18
|
+
options = {
|
|
19
|
+
ports: "21,22,23,25,53,80,110,139,143,443,445,3000,3306,3389,5000,8080,8081,8096,8888,9000",
|
|
20
|
+
threads: nil,
|
|
21
|
+
timeout: nil,
|
|
22
|
+
timing: 3,
|
|
23
|
+
discovery: true,
|
|
24
|
+
version_det: false,
|
|
25
|
+
udp: false,
|
|
26
|
+
os_detect: false,
|
|
27
|
+
noping: false,
|
|
28
|
+
output: nil,
|
|
29
|
+
output_json: nil,
|
|
30
|
+
output_grep: nil,
|
|
31
|
+
output_html: nil
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# Dukungan manual untuk -Pn dan -P (agar tidak bentrok dengan parser)
|
|
35
|
+
if ARGV.include?('-Pn') || ARGV.include?('-P')
|
|
36
|
+
options[:noping] = true
|
|
37
|
+
ARGV.delete('-Pn')
|
|
38
|
+
ARGV.delete('-P')
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
parser = OptionParser.new do |opts|
|
|
42
|
+
opts.banner = "Penggunaan: netmap [opsi] <target>"
|
|
43
|
+
opts.separator ""
|
|
44
|
+
opts.separator "Opsi Pemindaian:"
|
|
45
|
+
|
|
46
|
+
opts.on("-p", "--ports RANGE", "Rentang port (misal: 80,443 atau 1-1024)") do |v|
|
|
47
|
+
options[:ports] = v
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
opts.on("-s TYPE", "Jenis Scan (U: UDP, V: Versi, n: Ping)") do |v|
|
|
51
|
+
options[:udp] = true if v.include?('U')
|
|
52
|
+
options[:version_det] = true if v.include?('V')
|
|
53
|
+
options[:ports] = nil if v.include?('n')
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
opts.on("--udp", "Gunakan pemindaian UDP") { options[:udp] = true }
|
|
57
|
+
opts.on("--version-all", "Gunakan deteksi versi") { options[:version_det] = true }
|
|
58
|
+
opts.on("--ping", "Hanya host discovery") { options[:ports] = nil }
|
|
59
|
+
opts.on("--noping", "Lewati host discovery, anggap host up") { options[:noping] = true }
|
|
60
|
+
|
|
61
|
+
opts.on("-O", "--osscan", "Deteksi OS - Tebak sistem operasi target") do
|
|
62
|
+
options[:os_detect] = true
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
opts.separator ""
|
|
66
|
+
opts.separator "Opsi Kecepatan (Timing):"
|
|
67
|
+
|
|
68
|
+
opts.on("-T", "--timing TEMPLATE", Integer, "Template waktu 0-5 (default: 3)") do |v|
|
|
69
|
+
options[:timing] = v
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
opts.on("-t", "--threads JUMLAH", Integer, "Jumlah thread/concurrency") do |v|
|
|
73
|
+
options[:threads] = v
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
opts.separator ""
|
|
77
|
+
opts.separator "Opsi Output:"
|
|
78
|
+
|
|
79
|
+
opts.on("-n", "--output-normal FILE", "Simpan hasil ke file teks (Normal)") do |v|
|
|
80
|
+
options[:output] = v
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
opts.on("-j", "--json FILE", "Simpan hasil ke file JSON") do |v|
|
|
84
|
+
options[:output_json] = v
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
opts.on("-g", "--grepable FILE", "Simpan hasil ke format Grepable") do |v|
|
|
88
|
+
options[:output_grep] = v
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
opts.on("--html FILE", "Simpan hasil ke laporan HTML") do |v|
|
|
92
|
+
options[:output_html] = v
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
opts.on("-v", "--version", "Tampilkan versi aplikasi") do
|
|
96
|
+
puts BANNER
|
|
97
|
+
puts " NetMap Ultimate Version: #{NetMap::VERSION}".yellow
|
|
98
|
+
exit
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
opts.on("-h", "--help", "Tampilkan bantuan ini") do
|
|
102
|
+
puts BANNER
|
|
103
|
+
puts opts
|
|
104
|
+
exit
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
begin
|
|
109
|
+
parser.parse!(args)
|
|
110
|
+
exit 0 if args.empty? && (puts BANNER; puts parser; true)
|
|
111
|
+
run_scan(args, options)
|
|
112
|
+
rescue => e
|
|
113
|
+
puts "Error: #{e.message}".red
|
|
114
|
+
exit 1
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def self.run_scan(targets, options)
|
|
119
|
+
puts BANNER
|
|
120
|
+
puts "\n" + "=".center(75, "=").yellow
|
|
121
|
+
puts " MEMULAI PEMINDAIAN NETMAP ULTIMATE ".center(75, "=").yellow
|
|
122
|
+
puts "=".center(75, "=").yellow
|
|
123
|
+
puts " Waktu : #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}"
|
|
124
|
+
puts " Target : #{targets.join(', ')}"
|
|
125
|
+
puts " Mode : #{options[:udp] ? 'UDP Scan' : 'TCP Scan'}"
|
|
126
|
+
puts " OS Det : #{options[:os_detect] ? 'Aktif' : 'Non-aktif'}"
|
|
127
|
+
puts " Ping : #{options[:noping] ? 'Dinonaktifkan (-Pn)' : 'Aktif'}"
|
|
128
|
+
puts " Timing : -T#{options[:timing]}"
|
|
129
|
+
puts "=".center(75, "=").yellow + "\n"
|
|
130
|
+
|
|
131
|
+
scanner = NetMap::Scanner.new(
|
|
132
|
+
targets,
|
|
133
|
+
options[:ports],
|
|
134
|
+
threads: options[:threads],
|
|
135
|
+
timeout: options[:timeout],
|
|
136
|
+
timing: options[:timing],
|
|
137
|
+
discovery: options[:discovery],
|
|
138
|
+
udp: options[:udp],
|
|
139
|
+
os_detect: options[:os_detect],
|
|
140
|
+
noping: options[:noping]
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
start_time = Time.now
|
|
144
|
+
results = scanner.run
|
|
145
|
+
duration = (Time.now - start_time).round(2)
|
|
146
|
+
|
|
147
|
+
display_results(results, duration, scanner.active_hosts, scanner)
|
|
148
|
+
|
|
149
|
+
save_results(results, options[:output]) if options[:output]
|
|
150
|
+
save_json(results, options[:output_json]) if options[:output_json]
|
|
151
|
+
save_grepable(results, options[:output_grep]) if options[:output_grep]
|
|
152
|
+
save_html(results, options[:output_html]) if options[:output_html]
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def self.display_results(results, duration, active_hosts, scanner = nil)
|
|
156
|
+
if active_hosts.empty?
|
|
157
|
+
puts "\nTidak ada host aktif ditemukan.".red
|
|
158
|
+
elsif results.empty? && active_hosts.any?
|
|
159
|
+
# Jika pingscan saja (-sn), tampilkan tabel host yang ditemukan
|
|
160
|
+
puts "\n" + "── HASIL PENEMUAN HOST (PING SCAN) ──".center(75, " ").cyan
|
|
161
|
+
col_widths = { ip: 25, mac_vendor: 50 }
|
|
162
|
+
|
|
163
|
+
puts "┌#{'─' * (col_widths[:ip]+2)}┬#{'─' * (col_widths[:mac_vendor]+2)}┐"
|
|
164
|
+
puts "│ #{'ALAMAT IP'.ljust(col_widths[:ip])} │ #{'MAC / VENDOR'.ljust(col_widths[:mac_vendor])} │"
|
|
165
|
+
puts "├#{'─' * (col_widths[:ip]+2)}┼#{'─' * (col_widths[:mac_vendor]+2)}┤"
|
|
166
|
+
|
|
167
|
+
active_hosts.each do |ip|
|
|
168
|
+
arp_info = scanner&.instance_variable_get(:@arp_table)&.[](ip)
|
|
169
|
+
mac_vendor_text = if arp_info
|
|
170
|
+
mac = arp_info[:mac]
|
|
171
|
+
vendor = scanner.send(:get_vendor_by_mac, mac)
|
|
172
|
+
"#{mac} (#{vendor})"
|
|
173
|
+
else
|
|
174
|
+
"N/A (Cek tabel ARP manual)"
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
puts "│ #{ip.ljust(col_widths[:ip]).green} │ #{mac_vendor_text.ljust(col_widths[:mac_vendor]).light_black} │"
|
|
178
|
+
end
|
|
179
|
+
puts "└#{'─' * (col_widths[:ip]+2)}┴#{'─' * (col_widths[:mac_vendor]+2)}┘"
|
|
180
|
+
else
|
|
181
|
+
puts "\n"
|
|
182
|
+
# Dimensi Kolom: IP(15), Port(10), Status(15), OS(15), MAC(30), Banner(45)
|
|
183
|
+
col_widths = { ip: 15, port: 10, status: 15, os: 15, mac: 30, banner: 45 }
|
|
184
|
+
|
|
185
|
+
# Header
|
|
186
|
+
puts "┌#{'─' * (col_widths[:ip]+2)}┬#{'─' * (col_widths[:port]+2)}┬#{'─' * (col_widths[:status]+2)}┬#{'─' * (col_widths[:os]+2)}┬#{'─' * (col_widths[:mac]+2)}┬#{'─' * (col_widths[:banner]+2)}┐"
|
|
187
|
+
puts "│ #{'ALAMAT IP'.ljust(col_widths[:ip])} │ #{'PORT'.ljust(col_widths[:port])} │ #{'STATUS'.ljust(col_widths[:status])} │ #{'OS'.ljust(col_widths[:os])} │ #{'MAC / VENDOR'.ljust(col_widths[:mac])} │ #{'BANNER'.ljust(col_widths[:banner])} │"
|
|
188
|
+
puts "├#{'─' * (col_widths[:ip]+2)}┼#{'─' * (col_widths[:port]+2)}┼#{'─' * (col_widths[:status]+2)}┼#{'─' * (col_widths[:os]+2)}┼#{'─' * (col_widths[:mac]+2)}┼#{'─' * (col_widths[:banner]+2)}┤"
|
|
189
|
+
|
|
190
|
+
results.each do |r|
|
|
191
|
+
ip_val = r[:ip].ljust(col_widths[:ip]).green
|
|
192
|
+
port_val = "#{r[:port]}/#{r[:type]}".ljust(col_widths[:port]).yellow
|
|
193
|
+
status_color = r[:status] == :open ? :light_green : :yellow
|
|
194
|
+
status_val = r[:status].to_s.ljust(col_widths[:status]).send(status_color)
|
|
195
|
+
os_val = (r[:os] || "Unknown").ljust(col_widths[:os]).cyan
|
|
196
|
+
|
|
197
|
+
mac_vendor = r[:mac] ? "#{r[:mac]} (#{r[:vendor]})" : "N/A (Local only)"
|
|
198
|
+
mac_val = mac_vendor[0...(col_widths[:mac])].ljust(col_widths[:mac]).light_black
|
|
199
|
+
|
|
200
|
+
banner_text = (r[:banner] || "").to_s.gsub(/[^[:print:]]/, ' ').strip
|
|
201
|
+
banner_lines = wrap_text(banner_text, col_widths[:banner])
|
|
202
|
+
|
|
203
|
+
# Baris pertama (dengan semua data)
|
|
204
|
+
puts "│ #{ip_val} │ #{port_val} │ #{status_val} │ #{os_val} │ #{mac_val} │ #{banner_lines[0].ljust(col_widths[:banner]).italic} │"
|
|
205
|
+
|
|
206
|
+
# Baris tambahan jika banner dibungkus (wraped)
|
|
207
|
+
if banner_lines.size > 1
|
|
208
|
+
banner_lines[1..-1].each do |line|
|
|
209
|
+
puts "│ #{' ' * col_widths[:ip]} │ #{' ' * col_widths[:port]} │ #{' ' * col_widths[:status]} │ #{' ' * col_widths[:os]} │ #{' ' * col_widths[:mac]} │ #{line.ljust(col_widths[:banner]).italic} │"
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
puts "└#{'─' * (col_widths[:ip]+2)}┴#{'─' * (col_widths[:port]+2)}┴#{'─' * (col_widths[:status]+2)}┴#{'─' * (col_widths[:os]+2)}┴#{'─' * (col_widths[:mac]+2)}┴#{'─' * (col_widths[:banner]+2)}┘"
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
puts "\nSelesai dalam #{duration} detik. Total host aktif: #{active_hosts.size}. Port terbuka: #{results.size}".cyan
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def self.wrap_text(text, width)
|
|
220
|
+
return [text] if text.size <= width
|
|
221
|
+
lines = []
|
|
222
|
+
while text.size > width
|
|
223
|
+
# Cari spasi terdekat untuk pemotongan yang rapi
|
|
224
|
+
chunk_end = text.rindex(' ', width) || width
|
|
225
|
+
lines << text[0...chunk_end].strip
|
|
226
|
+
text = text[chunk_end..-1].strip
|
|
227
|
+
end
|
|
228
|
+
lines << text unless text.empty?
|
|
229
|
+
lines
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def self.save_json(results, filename)
|
|
233
|
+
require 'json'
|
|
234
|
+
File.write(filename, JSON.pretty_generate(results))
|
|
235
|
+
puts "Hasil JSON disimpan: #{filename}".green
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def self.save_grepable(results, filename)
|
|
239
|
+
content = results.map do |r|
|
|
240
|
+
"Host: #{r[:ip]}\tPorts: #{r[:port]}/#{r[:status]}/#{r[:type]}//#{r[:banner]}//\tOS: #{r[:os]}"
|
|
241
|
+
end.join("\n")
|
|
242
|
+
File.write(filename, content)
|
|
243
|
+
puts "Hasil Grepable disimpan: #{filename}".green
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def self.save_html(results, filename)
|
|
247
|
+
require_relative 'html_reporter'
|
|
248
|
+
NetMap::HTMLReporter.generate(results, filename)
|
|
249
|
+
puts "Laporan HTML disimpan: #{filename}".green
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def self.save_results(results, filename)
|
|
253
|
+
File.open(filename, "w") do |f|
|
|
254
|
+
f.puts "Hasil Pemindaian NetMap Ultimate - #{Time.now}"
|
|
255
|
+
f.puts "=" * 50
|
|
256
|
+
results.each { |r| f.puts "IP: #{r[:ip]} | Port: #{r[:port]}/#{r[:type]} | Status: #{r[:status]} | OS: #{r[:os]} | Banner: #{r[:banner]}" }
|
|
257
|
+
end
|
|
258
|
+
puts "Hasil Normal disimpan: #{filename}".green
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
module NetMap
|
|
2
|
+
class HTMLReporter
|
|
3
|
+
def self.generate(results, filename)
|
|
4
|
+
html = <<~HTML
|
|
5
|
+
<!DOCTYPE html>
|
|
6
|
+
<html lang="id">
|
|
7
|
+
<head>
|
|
8
|
+
<meta charset="UTF-8">
|
|
9
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
10
|
+
<title>Laporan Pemindaian NetMap Ultimate</title>
|
|
11
|
+
<style>
|
|
12
|
+
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: #f4f7f6; color: #333; margin: 0; padding: 20px; }
|
|
13
|
+
.container { max-width: 1000px; margin: auto; background: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
|
|
14
|
+
h1 { color: #2c3e50; border-bottom: 2px solid #3498db; padding-bottom: 10px; }
|
|
15
|
+
.stats { display: flex; gap: 20px; margin-bottom: 30px; }
|
|
16
|
+
.stat-card { flex: 1; background: #3498db; color: white; padding: 20px; border-radius: 8px; text-align: center; }
|
|
17
|
+
.stat-card.green { background: #27ae60; }
|
|
18
|
+
table { width: 100%; border-collapse: collapse; margin-top: 20px; }
|
|
19
|
+
th { background: #34495e; color: white; text-align: left; padding: 12px; }
|
|
20
|
+
td { padding: 12px; border-bottom: 1px solid #ddd; }
|
|
21
|
+
tr:hover { background-color: #f1f1f1; }
|
|
22
|
+
.status-open { color: #27ae60; font-weight: bold; }
|
|
23
|
+
.os-tag { background: #ecf0f1; padding: 2px 8px; border-radius: 4px; font-size: 0.9em; }
|
|
24
|
+
.mac-tag { font-family: monospace; color: #7f8c8d; font-size: 0.9em; }
|
|
25
|
+
footer { margin-top: 50px; text-align: center; font-size: 0.8em; color: #7f8c8d; }
|
|
26
|
+
</style>
|
|
27
|
+
</head>
|
|
28
|
+
<body>
|
|
29
|
+
<div class="container">
|
|
30
|
+
<h1>Laporan Pemindaian NetMap</h1>
|
|
31
|
+
<p>Waktu Pemindaian: #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}</p>
|
|
32
|
+
|
|
33
|
+
<div class="stats">
|
|
34
|
+
<div class="stat-card">
|
|
35
|
+
<h3>Total Port Terbuka</h3>
|
|
36
|
+
<p style="font-size: 2em; margin: 0;">#{results.size}</p>
|
|
37
|
+
</div>
|
|
38
|
+
<div class="stat-card green">
|
|
39
|
+
<h3>Target Unik</h3>
|
|
40
|
+
<p style="font-size: 2em; margin: 0;">#{results.map { |r| r[:ip] }.uniq.size}</p>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<table>
|
|
45
|
+
<thead>
|
|
46
|
+
<tr>
|
|
47
|
+
<th>Alamat IP</th>
|
|
48
|
+
<th>Port</th>
|
|
49
|
+
<th>Status</th>
|
|
50
|
+
<th>Sistem Operasi</th>
|
|
51
|
+
<th>MAC / Vendor</th>
|
|
52
|
+
<th>Layanan / Banner</th>
|
|
53
|
+
</tr>
|
|
54
|
+
</thead>
|
|
55
|
+
<tbody>
|
|
56
|
+
#{results.map { |r| "
|
|
57
|
+
<tr>
|
|
58
|
+
<td>#{r[:ip]}</td>
|
|
59
|
+
<td>#{r[:port]}/#{r[:type]}</td>
|
|
60
|
+
<td><span class='status-open'>#{r[:status]}</span></td>
|
|
61
|
+
<td><span class='os-tag'>#{r[:os] || 'Unknown'}</span></td>
|
|
62
|
+
<td><span class='mac-tag'>#{r[:mac] || 'N/A'} #{"(#{r[:vendor]})" if r[:vendor]}</span></td>
|
|
63
|
+
<td><code>#{r[:banner]}</code></td>
|
|
64
|
+
</tr>" }.join}
|
|
65
|
+
</tbody>
|
|
66
|
+
</table>
|
|
67
|
+
|
|
68
|
+
<footer>
|
|
69
|
+
Dihasilkan secara otomatis oleh NetMap Ultimate Upgrade - By IshikawaUta
|
|
70
|
+
</footer>
|
|
71
|
+
</div>
|
|
72
|
+
</body>
|
|
73
|
+
</html>
|
|
74
|
+
HTML
|
|
75
|
+
File.write(filename, html)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
require 'socket'
|
|
2
|
+
require 'ipaddr'
|
|
3
|
+
require 'timeout'
|
|
4
|
+
|
|
5
|
+
module NetMap
|
|
6
|
+
class Scanner
|
|
7
|
+
attr_reader :targets, :ports, :threads, :timeout, :timing_template, :discovery_mode, :active_hosts
|
|
8
|
+
|
|
9
|
+
TIMING_TEMPLATES = {
|
|
10
|
+
0 => { threads: 1, timeout: 10.0 }, # Paranoid
|
|
11
|
+
1 => { threads: 5, timeout: 5.0 }, # Sneaky
|
|
12
|
+
2 => { threads: 10, timeout: 2.0 }, # Polite
|
|
13
|
+
3 => { threads: 50, timeout: 1.0 }, # Normal
|
|
14
|
+
4 => { threads: 200, timeout: 0.8 }, # Aggressive
|
|
15
|
+
5 => { threads: 400, timeout: 0.5 } # Insane (Capped at 400 threads/0.5s)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
def initialize(targets, ports, threads: nil, timeout: nil, timing: 3, discovery: true, udp: false, os_detect: false, noping: false)
|
|
19
|
+
@timing_template = TIMING_TEMPLATES[timing] || TIMING_TEMPLATES[3]
|
|
20
|
+
@targets = parse_targets(targets)
|
|
21
|
+
@ports = parse_ports(ports)
|
|
22
|
+
@threads = threads || @timing_template[:threads]
|
|
23
|
+
@timeout = timeout || @timing_template[:timeout]
|
|
24
|
+
@discovery_mode = discovery
|
|
25
|
+
@udp_mode = udp
|
|
26
|
+
@os_detect_mode = os_detect
|
|
27
|
+
@noping_mode = noping
|
|
28
|
+
@active_hosts = []
|
|
29
|
+
@results = []
|
|
30
|
+
|
|
31
|
+
# Inisialisasi Script Engine
|
|
32
|
+
require_relative 'script_engine'
|
|
33
|
+
script_dir = File.join(File.expand_path('../../../', __FILE__), 'scripts')
|
|
34
|
+
@script_engine = ScriptEngine.new(script_dir)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def run
|
|
38
|
+
@arp_table = get_arp_table # Pastikan tabel ARP selalu terisi
|
|
39
|
+
|
|
40
|
+
if @noping_mode
|
|
41
|
+
@active_hosts = @targets
|
|
42
|
+
puts "Mode -Pn aktif: Melewati Host Discovery, menganggap host ALIVE.".yellow
|
|
43
|
+
elsif @discovery_mode
|
|
44
|
+
puts "Melakukan Host Discovery (mencari host aktif)...".yellow
|
|
45
|
+
discover_hosts
|
|
46
|
+
puts "Host aktif ditemukan: #{@active_hosts.size}".green
|
|
47
|
+
return [] if @ports.empty?
|
|
48
|
+
else
|
|
49
|
+
@active_hosts = @targets
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
return [] if @active_hosts.empty?
|
|
53
|
+
|
|
54
|
+
scan_type = @udp_mode ? "UDP" : "TCP"
|
|
55
|
+
puts "Memulai pemindaian port #{scan_type} pada #{@active_hosts.size} host (Async Mode)...".yellow
|
|
56
|
+
|
|
57
|
+
tasks_list = []
|
|
58
|
+
@active_hosts.each { |ip| @ports.each { |port| tasks_list << { ip: ip, port: port } } }
|
|
59
|
+
total_tasks = tasks_list.size
|
|
60
|
+
completed = 0
|
|
61
|
+
|
|
62
|
+
require 'async'
|
|
63
|
+
require 'async/barrier'
|
|
64
|
+
require 'async/semaphore'
|
|
65
|
+
|
|
66
|
+
Async do |task|
|
|
67
|
+
barrier = Async::Barrier.new
|
|
68
|
+
semaphore = Async::Semaphore.new(@threads, parent: barrier)
|
|
69
|
+
|
|
70
|
+
tasks_list.each do |t|
|
|
71
|
+
semaphore.async do
|
|
72
|
+
if @udp_mode
|
|
73
|
+
scan_udp_port(t[:ip], t[:port])
|
|
74
|
+
else
|
|
75
|
+
scan_port(t[:ip], t[:port])
|
|
76
|
+
end
|
|
77
|
+
completed += 1
|
|
78
|
+
print_progress(completed, total_tasks) if total_tasks > 10
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
barrier.wait
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
puts "\n" if total_tasks > 10
|
|
85
|
+
|
|
86
|
+
# Refresh tabel ARP (Setelah koneksi dibuat, sistem operasi mengupdate cache ARP)
|
|
87
|
+
@arp_table = get_arp_table
|
|
88
|
+
|
|
89
|
+
# Tambahkan info MAC dan Vendor ke setiap hasil
|
|
90
|
+
@results.each do |r|
|
|
91
|
+
arp_info = @arp_table[r[:ip]]
|
|
92
|
+
if arp_info
|
|
93
|
+
r[:mac] = arp_info[:mac]
|
|
94
|
+
r[:vendor] = get_vendor_by_mac(r[:mac])
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
@results.sort_by { |r| [r[:ip], r[:port]] }
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private
|
|
102
|
+
|
|
103
|
+
def scan_udp_port(ip, port)
|
|
104
|
+
# UDP Scan: Best effort
|
|
105
|
+
begin
|
|
106
|
+
family = ip.include?(':') ? Socket::AF_INET6 : Socket::AF_INET
|
|
107
|
+
socket = Socket.new(family, Socket::SOCK_DGRAM, 0)
|
|
108
|
+
sockaddr = Socket.pack_sockaddr_in(port, ip)
|
|
109
|
+
|
|
110
|
+
socket.send("", 0, sockaddr)
|
|
111
|
+
|
|
112
|
+
if IO.select([socket], nil, nil, @timeout)
|
|
113
|
+
socket.recvfrom(512)
|
|
114
|
+
banner = format_banner("UDP Response Detected", port)
|
|
115
|
+
res = { ip: ip, port: port, status: :open, banner: banner, type: "UDP" }
|
|
116
|
+
@script_engine.run_scripts(res)
|
|
117
|
+
@results << res
|
|
118
|
+
else
|
|
119
|
+
banner = format_banner("", port)
|
|
120
|
+
banner = "No Response" if banner == "Penyajian banner gagal"
|
|
121
|
+
res = { ip: ip, port: port, status: :"open|filtered", banner: banner, type: "UDP" }
|
|
122
|
+
@script_engine.run_scripts(res)
|
|
123
|
+
@results << res
|
|
124
|
+
end
|
|
125
|
+
socket.close
|
|
126
|
+
rescue Errno::ECONNREFUSED
|
|
127
|
+
# Port UDP tertutup
|
|
128
|
+
rescue => _e
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
private
|
|
133
|
+
|
|
134
|
+
def discover_hosts
|
|
135
|
+
@arp_table = get_arp_table
|
|
136
|
+
|
|
137
|
+
# 1. ICMP Ping paralel (Cepat)
|
|
138
|
+
puts "[1] Mencoba ICMP Ping...".light_black
|
|
139
|
+
discover_hosts_icmp
|
|
140
|
+
|
|
141
|
+
# 2. Cross-reference dengan Tabel ARP (Layer 2)
|
|
142
|
+
puts "[2] Memeriksa Tabel ARP...".light_black
|
|
143
|
+
discover_hosts_arp
|
|
144
|
+
|
|
145
|
+
# 3. TCP Ping (Fallback untuk firewall ketat)
|
|
146
|
+
if @active_hosts.size < @targets.size
|
|
147
|
+
puts "[3] Mencoba TCP Ping (Port Umum)...".light_black
|
|
148
|
+
discover_hosts_tcp
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
@active_hosts.uniq!
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def discover_hosts_icmp
|
|
155
|
+
require 'async'
|
|
156
|
+
require 'async/barrier'
|
|
157
|
+
require 'async/semaphore'
|
|
158
|
+
require 'open3'
|
|
159
|
+
|
|
160
|
+
Async do |task|
|
|
161
|
+
barrier = Async::Barrier.new
|
|
162
|
+
semaphore = Async::Semaphore.new(50, parent: barrier) # Limit parallel pings
|
|
163
|
+
mutex = Mutex.new
|
|
164
|
+
|
|
165
|
+
@targets.each do |ip|
|
|
166
|
+
semaphore.async do
|
|
167
|
+
# Gunakan perintah ping sistem (-W timeout -c jumlah)
|
|
168
|
+
_stdout, _stderr, status = Open3.capture3("ping -c 1 -W 0.2 #{ip}")
|
|
169
|
+
if status.success?
|
|
170
|
+
mutex.synchronize { @active_hosts << ip }
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
barrier.wait
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def discover_hosts_arp
|
|
179
|
+
@targets.each do |ip|
|
|
180
|
+
# Jika IP ada di tabel ARP dan statusnya bukan incomplete (0x0), anggap UP
|
|
181
|
+
if @arp_table[ip] && @arp_table[ip][:mac] != "00:00:00:00:00:00"
|
|
182
|
+
@active_hosts << ip
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def discover_hosts_tcp
|
|
188
|
+
require 'async'
|
|
189
|
+
require 'async/barrier'
|
|
190
|
+
require 'async/semaphore'
|
|
191
|
+
|
|
192
|
+
ping_ports = [80, 443, 22, 445, 3389, 8080]
|
|
193
|
+
Async do |task|
|
|
194
|
+
barrier = Async::Barrier.new
|
|
195
|
+
semaphore = Async::Semaphore.new(@threads, parent: barrier)
|
|
196
|
+
mutex = Mutex.new
|
|
197
|
+
|
|
198
|
+
@targets.each do |ip|
|
|
199
|
+
next if @active_hosts.include?(ip)
|
|
200
|
+
semaphore.async do
|
|
201
|
+
is_up = false
|
|
202
|
+
ping_ports.each do |port|
|
|
203
|
+
begin
|
|
204
|
+
family = ip.include?(':') ? Socket::AF_INET6 : Socket::AF_INET
|
|
205
|
+
s = Socket.new(family, Socket::SOCK_STREAM, 0)
|
|
206
|
+
sockaddr = Socket.pack_sockaddr_in(port, ip)
|
|
207
|
+
s.connect_nonblock(sockaddr) rescue nil
|
|
208
|
+
if IO.select(nil, [s], nil, 0.2)
|
|
209
|
+
is_up = true; s.close; break
|
|
210
|
+
end
|
|
211
|
+
s.close
|
|
212
|
+
rescue; next; end
|
|
213
|
+
end
|
|
214
|
+
mutex.synchronize { @active_hosts << ip } if is_up
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
barrier.wait
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def get_arp_table
|
|
222
|
+
table = {}
|
|
223
|
+
return table unless File.exist?('/proc/net/arp')
|
|
224
|
+
|
|
225
|
+
File.readlines('/proc/net/arp').drop(1).each do |line|
|
|
226
|
+
parts = line.split(/\s+/)
|
|
227
|
+
# IP address (0), HW type (1), Flags (2), HW address (3), Mask (4), Device (5)
|
|
228
|
+
table[parts[0]] = { mac: parts[3], device: parts[5] }
|
|
229
|
+
end
|
|
230
|
+
table
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def get_vendor_by_mac(mac)
|
|
234
|
+
return "Unknown" if mac.nil? || mac == "00:00:00:00:00:00"
|
|
235
|
+
|
|
236
|
+
# Deteksi Locally Administered Address (LAA) - Umum di Docker/VM
|
|
237
|
+
# Oktet pertama memiliki bit kedua bernilai 1 (x2, x6, xA, xE)
|
|
238
|
+
first_octet = mac.split(/[:-]/)[0].to_i(16)
|
|
239
|
+
return "Virtual/Randomized (LAA)" if (first_octet & 0b10) != 0
|
|
240
|
+
|
|
241
|
+
prefix = mac.gsub(/[:-]/, '').upcase[0..5]
|
|
242
|
+
|
|
243
|
+
# OUI Mapping Gede-gedean
|
|
244
|
+
vendors = {
|
|
245
|
+
"00000C" => "Cisco", "000502" => "Apple", "000C29" => "VMware",
|
|
246
|
+
"001422" => "Dell", "00163E" => "Xen/AWS", "001A11" => "Google",
|
|
247
|
+
"005056" => "VMware", "0242AC" => "Docker", "080027" => "Oracle/VB",
|
|
248
|
+
"18AF61" => "Apple", "20DFB9" => "Samsung", "3C5AB4" => "Google",
|
|
249
|
+
"44650D" => "Amazon", "525400" => "QEMU/KVM", "B827EB" => "Raspberry Pi",
|
|
250
|
+
"DCA632" => "Raspberry Pi", "F4F5D8" => "Google", "F01898" => "Apple",
|
|
251
|
+
"000142" => "Cisco", "0002B3" => "Intel", "00040E" => "Linksys",
|
|
252
|
+
"000CF1" => "Intel", "001310" => "Linksys", "001565" => "Yealink",
|
|
253
|
+
"00216A" => "Intel", "0026B9" => "Dell", "3052CB" => "Liteon (Acer/HP)",
|
|
254
|
+
"408D5C" => "Giga-Byte", "5065F3" => "TP-Link", "60A44C" => "ASUSTek",
|
|
255
|
+
"708BC0" => "Intel", "80E650" => "Intel", "90F1AA" => "TP-Link",
|
|
256
|
+
"A42BB0" => "TP-Link", "C02568" => "TP-Link", "E4F042" => "Xiaomi",
|
|
257
|
+
"E84E06" => "TP-Link", "F8FF12" => "Apple", "F43167" => "TP-Link"
|
|
258
|
+
}
|
|
259
|
+
vendors[prefix] || "Unknown Vendor"
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def parse_targets(targets_raw)
|
|
263
|
+
ips = []
|
|
264
|
+
targets_raw.each do |target|
|
|
265
|
+
begin
|
|
266
|
+
if target.include?('/')
|
|
267
|
+
# Mendukung CIDR IPv4 dan IPv6
|
|
268
|
+
# Untuk IPv6 / notation, IPAddr ruby mendukungnya
|
|
269
|
+
addr = IPAddr.new(target)
|
|
270
|
+
addr.to_range.each { |ip| ips << ip.to_s }
|
|
271
|
+
else
|
|
272
|
+
ips << IPAddr.new(target).to_s
|
|
273
|
+
end
|
|
274
|
+
rescue ArgumentError
|
|
275
|
+
puts "Peringatan: Format IP tidak valid - #{target}".red
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
ips.uniq
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def parse_ports(ports_raw)
|
|
282
|
+
return [] if ports_raw.nil? || ports_raw.empty?
|
|
283
|
+
ports = []
|
|
284
|
+
ports_raw.split(',').each do |p|
|
|
285
|
+
if p.include?('-')
|
|
286
|
+
low, high = p.split('-').map(&:to_i)
|
|
287
|
+
(low..high).each { |port| ports << port }
|
|
288
|
+
else
|
|
289
|
+
ports << p.to_i
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
ports.uniq.select { |p| p > 0 && p < 65536 }
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def scan_port(ip, port)
|
|
296
|
+
begin
|
|
297
|
+
family = ip.include?(':') ? Socket::AF_INET6 : Socket::AF_INET
|
|
298
|
+
# Gunakan Timeout eksplisit untuk keamanan
|
|
299
|
+
Timeout.timeout(@timeout + 0.5) do
|
|
300
|
+
socket = Socket.new(family, Socket::SOCK_STREAM, 0)
|
|
301
|
+
sockaddr = Socket.pack_sockaddr_in(port, ip)
|
|
302
|
+
|
|
303
|
+
begin
|
|
304
|
+
socket.connect_nonblock(sockaddr)
|
|
305
|
+
rescue IO::WaitWritable
|
|
306
|
+
# Gunakan buffer timeout sedikit lebih lama dari global untuk IO select
|
|
307
|
+
select_timeout = [@timeout, 0.5].max
|
|
308
|
+
unless IO.select(nil, [socket], nil, select_timeout)
|
|
309
|
+
socket.close
|
|
310
|
+
raise Timeout::Error
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# Verifikasi koneksi setelah select
|
|
314
|
+
begin
|
|
315
|
+
socket.connect_nonblock(sockaddr)
|
|
316
|
+
rescue Errno::EISCONN
|
|
317
|
+
# Koneksi sudah mapan
|
|
318
|
+
rescue => e
|
|
319
|
+
socket.close
|
|
320
|
+
raise e
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# Jika sampai sini, berarti koneksi berhasil
|
|
325
|
+
os_info = @os_detect_mode ? detect_os(socket) : nil
|
|
326
|
+
banner = probe_service(ip, port, socket)
|
|
327
|
+
socket.close
|
|
328
|
+
|
|
329
|
+
result = { ip: ip, port: port, status: :open, banner: banner, os: os_info, type: "TCP" }
|
|
330
|
+
@script_engine.run_scripts(result)
|
|
331
|
+
@results << result
|
|
332
|
+
end
|
|
333
|
+
rescue Timeout::Error, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Errno::ETIMEDOUT, Errno::EINVAL
|
|
334
|
+
# Port tertutup atau tidak terjangkau
|
|
335
|
+
rescue => _e
|
|
336
|
+
# Error lainnya (termasuk socket tertutup)
|
|
337
|
+
end
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def detect_os(socket)
|
|
341
|
+
begin
|
|
342
|
+
# Tebak OS berdasarkan TTL paket yang diterima
|
|
343
|
+
# Di Linux/Unix, kita bisa mengambilnya via IP_TTL
|
|
344
|
+
ttl = socket.getsockopt(Socket::IPPROTO_IP, Socket::IP_TTL).int rescue nil
|
|
345
|
+
return "Unknown" unless ttl
|
|
346
|
+
|
|
347
|
+
case ttl
|
|
348
|
+
when 0..64 then "Linux/Unix"
|
|
349
|
+
when 65..128 then "Windows"
|
|
350
|
+
when 129..255 then "Cisco/Solaris"
|
|
351
|
+
else "Unknown (#{ttl})"
|
|
352
|
+
end
|
|
353
|
+
rescue
|
|
354
|
+
"Detection Failed"
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def probe_service(ip, port, socket)
|
|
359
|
+
# Daftar port yang kemungkinan besar adalah HTTP
|
|
360
|
+
http_ports = [80, 81, 8080, 8081, 8096, 443, 8443, 3000, 3001, 5000, 8888, 9000]
|
|
361
|
+
|
|
362
|
+
banner = grab_banner(socket)
|
|
363
|
+
|
|
364
|
+
if http_ports.include?(port) || banner.empty?
|
|
365
|
+
# Coba probe HTTP jika port umum atau jika banner kosong (silent service)
|
|
366
|
+
http_info = grab_http_info(socket, port)
|
|
367
|
+
return http_info unless http_info.empty?
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
format_banner(banner, port)
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
def format_banner(banner, port)
|
|
374
|
+
# Jika banner kosong, kita tetap coba identifikasi via port
|
|
375
|
+
clean_banner = (banner || "").gsub(/[^[:print:]]/, ' ')
|
|
376
|
+
|
|
377
|
+
case port
|
|
378
|
+
when 3306
|
|
379
|
+
return "Database: MySQL/MariaDB" if clean_banner.strip.empty?
|
|
380
|
+
if clean_banner =~ /(\d+\.\d+\.\d+-(?:MariaDB|MySQL)[^\s]*)/
|
|
381
|
+
"Database: #{$1}"
|
|
382
|
+
else
|
|
383
|
+
"MySQL/MariaDB Database"
|
|
384
|
+
end
|
|
385
|
+
when 22
|
|
386
|
+
return "SSH Service" if clean_banner.strip.empty?
|
|
387
|
+
clean_banner[/SSH-\d+\.\d+-([^\s]*)/] ? "SSH: #{$1}" : clean_banner
|
|
388
|
+
when 53
|
|
389
|
+
"Domain Name System (DNS)"
|
|
390
|
+
when 80, 8080, 443, 8443
|
|
391
|
+
clean_banner.strip.empty? ? "HTTP Service" : clean_banner.strip
|
|
392
|
+
else
|
|
393
|
+
clean_banner.strip.empty? ? "Penyajian banner gagal" : clean_banner.strip
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
def grab_http_info(socket, port)
|
|
398
|
+
begin
|
|
399
|
+
# Gunakan print dan flush untuk memastikan data terkirim
|
|
400
|
+
request = "GET / HTTP/1.1\r\n" \
|
|
401
|
+
"Host: 127.0.0.1\r\n" \
|
|
402
|
+
"User-Agent: NetMap/2.0\r\n" \
|
|
403
|
+
"Accept: */*\r\n" \
|
|
404
|
+
"Connection: close\r\n" \
|
|
405
|
+
"\r\n"
|
|
406
|
+
|
|
407
|
+
socket.print(request)
|
|
408
|
+
socket.flush
|
|
409
|
+
|
|
410
|
+
response = ""
|
|
411
|
+
# Timeout sedikit lebih lama untuk server yang lambat memproses
|
|
412
|
+
Timeout.timeout(2.0) do
|
|
413
|
+
# Baca sampai socket ditutup atau timeout
|
|
414
|
+
while (chunk = socket.read(1024))
|
|
415
|
+
response << chunk
|
|
416
|
+
break if response.include?("\r\n\r\n") || response.size > 8192
|
|
417
|
+
end
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
return "" if response.empty?
|
|
421
|
+
|
|
422
|
+
# Ekstrak informasi
|
|
423
|
+
server = response[/Server: (.*)/i, 1]
|
|
424
|
+
title = response[/<title>(.*)<\/title>/im, 1]
|
|
425
|
+
|
|
426
|
+
info = []
|
|
427
|
+
if server
|
|
428
|
+
if server.include?("Eksa") || server.include?("Eks-Cent") || response.include?("EKSA")
|
|
429
|
+
info << "Eksa Server (#{server.strip})"
|
|
430
|
+
else
|
|
431
|
+
info << "HTTP Server: #{server.strip}"
|
|
432
|
+
end
|
|
433
|
+
elsif response.include?("Eksa Server") || response.include?("Eks-Cent")
|
|
434
|
+
info << "Eksa Server (Detected)"
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
info << "Title: #{title.strip}" if title
|
|
438
|
+
|
|
439
|
+
# Jika tidak ada info spesifik tapi berhasil konek HTTP
|
|
440
|
+
if info.empty? && response.include?("HTTP/")
|
|
441
|
+
status = response[/\AHTTP\/1\.\d (\d+)/, 1]
|
|
442
|
+
info << "HTTP Service (Status: #{status})"
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
info.compact.join(' | ')
|
|
446
|
+
rescue => _e
|
|
447
|
+
""
|
|
448
|
+
end
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
def grab_banner(socket)
|
|
452
|
+
begin
|
|
453
|
+
socket.read_nonblock(512).strip.gsub(/[^[:print:]]/, '')
|
|
454
|
+
rescue IO::WaitReadable
|
|
455
|
+
IO.select([socket], nil, nil, 0.5)
|
|
456
|
+
retry if IO.select([socket], nil, nil, 0.1)
|
|
457
|
+
""
|
|
458
|
+
rescue
|
|
459
|
+
""
|
|
460
|
+
end
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
def print_progress(current, total)
|
|
464
|
+
percent = (current.to_f / total * 100).round(1)
|
|
465
|
+
print "\rMemproses: #{percent}% (#{current}/#{total})".cyan
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
def yield_result(result)
|
|
469
|
+
# Placeholder untuk real-time reporting jika dibutuhkan
|
|
470
|
+
end
|
|
471
|
+
end
|
|
472
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
module NetMap
|
|
2
|
+
class ScriptEngine
|
|
3
|
+
def initialize(script_dir)
|
|
4
|
+
@script_dir = script_dir
|
|
5
|
+
@scripts = load_scripts
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def load_scripts
|
|
9
|
+
scripts = []
|
|
10
|
+
return scripts unless Dir.exist?(@script_dir)
|
|
11
|
+
|
|
12
|
+
Dir.glob(File.join(@script_dir, "*.rb")).each do |script_path|
|
|
13
|
+
begin
|
|
14
|
+
script_code = File.read(script_path)
|
|
15
|
+
scripts << { name: File.basename(script_path), code: script_code }
|
|
16
|
+
rescue => e
|
|
17
|
+
puts "Error loading script #{script_path}: #{e.message}".red
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
scripts
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def run_scripts(result)
|
|
24
|
+
@scripts.each do |script|
|
|
25
|
+
begin
|
|
26
|
+
# Lingkungan eksekusi skrip sederhana
|
|
27
|
+
# Skrip harus mengembalikan string jika ada temuan, atau nil
|
|
28
|
+
# Konteks: ip, port, banner, os
|
|
29
|
+
context = result
|
|
30
|
+
finding = eval_script(script[:code], context)
|
|
31
|
+
if finding && !finding.empty?
|
|
32
|
+
result[:banner] = "#{result[:banner]} | [#{script[:name]}: #{finding}]"
|
|
33
|
+
end
|
|
34
|
+
rescue
|
|
35
|
+
# Abaikan error skrip agar scan tetap jalan
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def eval_script(code, context)
|
|
43
|
+
# Simulasikan DSL skrip sederhana menggunakan instance_eval pada context object
|
|
44
|
+
# Ini menghindari peringatan "unused variable" dan memberikan sandbox ringan
|
|
45
|
+
dsl_context = Object.new
|
|
46
|
+
context.each do |key, value|
|
|
47
|
+
dsl_context.define_singleton_method(key) { value }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
dsl_context.instance_eval(code)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
data/lib/netmap.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: netmap-scanner
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- IshikawaUta
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: colorize
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.1'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '1.1'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: async
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '2.38'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '2.38'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: async-io
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '1.43'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '1.43'
|
|
54
|
+
description: NetMap is a powerful network scanner featuring parallel host discovery,
|
|
55
|
+
OS detection, service fingerprinting, and a custom Ruby scripting engine.
|
|
56
|
+
email:
|
|
57
|
+
- komikers09@gmail.com
|
|
58
|
+
executables:
|
|
59
|
+
- netmap
|
|
60
|
+
extensions: []
|
|
61
|
+
extra_rdoc_files: []
|
|
62
|
+
files:
|
|
63
|
+
- LICENSE
|
|
64
|
+
- README.md
|
|
65
|
+
- assets/logo.png
|
|
66
|
+
- bin/netmap
|
|
67
|
+
- lib/netmap.rb
|
|
68
|
+
- lib/netmap/cli.rb
|
|
69
|
+
- lib/netmap/html_reporter.rb
|
|
70
|
+
- lib/netmap/scanner.rb
|
|
71
|
+
- lib/netmap/script_engine.rb
|
|
72
|
+
- lib/netmap/version.rb
|
|
73
|
+
- scripts/http_check.rb
|
|
74
|
+
homepage: https://github.com/IshikawaUta
|
|
75
|
+
licenses:
|
|
76
|
+
- MIT
|
|
77
|
+
metadata:
|
|
78
|
+
homepage_uri: https://github.com/IshikawaUta
|
|
79
|
+
source_code_uri: https://github.com/IshikawaUta/netmap-scanner
|
|
80
|
+
rdoc_options: []
|
|
81
|
+
require_paths:
|
|
82
|
+
- lib
|
|
83
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
84
|
+
requirements:
|
|
85
|
+
- - ">="
|
|
86
|
+
- !ruby/object:Gem::Version
|
|
87
|
+
version: 3.0.0
|
|
88
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
89
|
+
requirements:
|
|
90
|
+
- - ">="
|
|
91
|
+
- !ruby/object:Gem::Version
|
|
92
|
+
version: '0'
|
|
93
|
+
requirements: []
|
|
94
|
+
rubygems_version: 3.6.7
|
|
95
|
+
specification_version: 4
|
|
96
|
+
summary: High-performance Network Scanner with Scripting Engine
|
|
97
|
+
test_files: []
|