Skip to content

Commit 841e5ab

Browse files
committed
Varification of v2.0
1 parent 2a3574b commit 841e5ab

19 files changed

Lines changed: 641 additions & 27 deletions

README.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -193,10 +193,19 @@ When to use: bring up the full local operator surface in one command with health
193193
`connect`, `udp`, and `service` scans work without raw-socket privileges.
194194
Raw-style TCP modes such as `syn`, `ack`, `fin`, `null`, `xmas`, `window`, and `maimon` need both:
195195

196-
- elevated privileges such as `sudo`
197-
- a real raw-capable TCP probe backend
196+
- elevated privileges such as `sudo` or an Administrator shell
197+
- a real raw-capable TCP probe backend such as `nping`
198198

199-
The current bundled scanner backend is still connect-oriented for TCP probes, so `sudo` alone does not make those raw modes equivalent to Nmap.
199+
ASRFacet-Rb now supports `nping` as the raw TCP backend across Linux, macOS, and Windows.
200+
201+
- Linux and macOS: install `nping`, then use `--raw-backend nping --sudo`
202+
- Windows: install `nping` with `Npcap`, then run from an elevated Administrator shell or use `--sudo` so the CLI can request elevation
203+
204+
Example:
205+
206+
```bash
207+
asrfacet-rb portscan 192.0.2.10 --type xmas --raw-backend nping --sudo
208+
```
200209

201210
## Output, Storage, and Reporting
202211

asrfacet-rb.gemspec

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ require_relative "lib/asrfacet_rb/version"
1616
require_relative "lib/asrfacet_rb/metadata"
1717

1818
Gem::Specification.new do |spec|
19+
generated_prefixes = %w[install/test-root/ output/ tmp/ vendor/].freeze
20+
packaged_files = Dir.glob("{bin,config,docker,install,lib,man,spec,test,wordlists}/**/*").select do |path|
21+
File.file?(path) && generated_prefixes.none? { |prefix| path.start_with?(prefix) }
22+
end
23+
1924
spec.name = "asrfacet-rb"
2025
spec.version = ASRFacet::VERSION
2126
spec.authors = [ASRFacet::Metadata::AUTHOR]
@@ -26,7 +31,16 @@ Gem::Specification.new do |spec|
2631
spec.license = "LicenseRef-Proprietary"
2732
spec.required_ruby_version = ">= 3.2"
2833

29-
spec.files = Dir.glob("{bin,config,docker,install,lib,man,spec,test,wordlists}/**/*").select { |path| File.file?(path) } + %w[Gemfile README.md LICENSE Rakefile Procfile .dockerignore]
34+
spec.files = packaged_files + %w[
35+
Gemfile
36+
README.md
37+
LICENSE
38+
Rakefile
39+
Procfile
40+
.dockerignore
41+
temp/nmap/nmap-service-probes
42+
temp/nmap/nmap-services
43+
]
3044
spec.bindir = "bin"
3145
spec.executables = %w[asrfacet-rb asrfrb]
3246
spec.require_paths = ["lib"]
@@ -47,8 +61,9 @@ Gem::Specification.new do |spec|
4761
spec.add_runtime_dependency "webrick", ">= 1.7", "< 2"
4862
spec.add_runtime_dependency "concurrent-ruby", ">= 1.2", "< 2"
4963
spec.add_runtime_dependency "csv", ">= 3.2", "< 4"
50-
spec.add_runtime_dependency "caracal", ">= 1.5", "< 2"
51-
spec.add_runtime_dependency "hexapdf", ">= 0.36", "< 1"
64+
spec.add_runtime_dependency "base64", ">= 0.3", "< 1"
65+
spec.add_runtime_dependency "caracal", ">= 1.4", "< 2"
66+
spec.add_runtime_dependency "hexapdf", ">= 0.24", "< 2"
5267
spec.add_development_dependency "rake"
5368
spec.add_development_dependency "rspec"
5469
spec.add_development_dependency "webmock"

install/linux.sh

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ RUNTIME_PAYLOAD=(
4040
"config"
4141
"lib"
4242
"man"
43+
"temp/nmap/nmap-service-probes"
44+
"temp/nmap/nmap-services"
4345
"Gemfile"
4446
"Gemfile.lock"
4547
"README.md"
@@ -120,7 +122,17 @@ copy_payload() {
120122
run_cmd mkdir -p "$destination_root"
121123
local entry
122124
for entry in "${RUNTIME_PAYLOAD[@]}"; do
123-
[ -e "$REPO_ROOT/$entry" ] && run_cmd cp -R "$REPO_ROOT/$entry" "$destination_root/"
125+
[ -e "$REPO_ROOT/$entry" ] || continue
126+
127+
local destination_dir="$destination_root"
128+
local relative_dir
129+
relative_dir="$(dirname "$entry")"
130+
if [ "$relative_dir" != "." ]; then
131+
destination_dir="$destination_root/$relative_dir"
132+
run_cmd mkdir -p "$destination_dir"
133+
fi
134+
135+
run_cmd cp -R "$REPO_ROOT/$entry" "$destination_dir/"
124136
done
125137

126138
[ -d "$REPO_ROOT/wordlists" ] && run_cmd cp -R "$REPO_ROOT/wordlists" "$destination_root/"

install/macos.sh

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ RUNTIME_PAYLOAD=(
4040
"config"
4141
"lib"
4242
"man"
43+
"temp/nmap/nmap-service-probes"
44+
"temp/nmap/nmap-services"
4345
"Gemfile"
4446
"Gemfile.lock"
4547
"README.md"
@@ -120,7 +122,17 @@ copy_payload() {
120122
run_cmd mkdir -p "$destination_root"
121123
local entry
122124
for entry in "${RUNTIME_PAYLOAD[@]}"; do
123-
[ -e "$REPO_ROOT/$entry" ] && run_cmd cp -R "$REPO_ROOT/$entry" "$destination_root/"
125+
[ -e "$REPO_ROOT/$entry" ] || continue
126+
127+
local destination_dir="$destination_root"
128+
local relative_dir
129+
relative_dir="$(dirname "$entry")"
130+
if [ "$relative_dir" != "." ]; then
131+
destination_dir="$destination_root/$relative_dir"
132+
run_cmd mkdir -p "$destination_dir"
133+
fi
134+
135+
run_cmd cp -R "$REPO_ROOT/$entry" "$destination_dir/"
124136
done
125137

126138
[ -d "$REPO_ROOT/wordlists" ] && run_cmd cp -R "$REPO_ROOT/wordlists" "$destination_root/"

install/windows.ps1

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ $RuntimePayload = @(
4444
"config",
4545
"lib",
4646
"man",
47+
"temp/nmap/nmap-service-probes",
48+
"temp/nmap/nmap-services",
4749
"Gemfile",
4850
"Gemfile.lock",
4951
"README.md",
@@ -220,7 +222,16 @@ function Copy-Payload {
220222
foreach ($entry in $RuntimePayload) {
221223
$source = Join-Path $RepoRoot $entry
222224
if (Test-Path -LiteralPath $source) {
223-
Copy-Item -LiteralPath $source -Destination $DestinationRoot -Recurse -Force
225+
$entryParent = Split-Path -Parent $entry
226+
$destination = if ([string]::IsNullOrWhiteSpace($entryParent)) {
227+
$DestinationRoot
228+
} else {
229+
$targetDir = Join-Path $DestinationRoot $entryParent
230+
New-Item -ItemType Directory -Path $targetDir -Force | Out-Null
231+
$targetDir
232+
}
233+
234+
Copy-Item -LiteralPath $source -Destination $destination -Recurse -Force
224235
}
225236
}
226237

lib/asrfacet_rb.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,10 +96,12 @@
9696
require_relative "asrfacet_rb/scanner/results/port_result"
9797
require_relative "asrfacet_rb/scanner/results/host_result"
9898
require_relative "asrfacet_rb/scanner/results/scan_result"
99+
require_relative "asrfacet_rb/scanner/platform"
99100
require_relative "asrfacet_rb/scanner/privilege"
100101
require_relative "asrfacet_rb/scanner/verbose_logger"
101102
require_relative "asrfacet_rb/scanner/probe_db"
102103
require_relative "asrfacet_rb/scanner/result_adapter"
104+
require_relative "asrfacet_rb/scanner/probes/nping_raw_adapter"
103105
require_relative "asrfacet_rb/scanner/probes/tcp_prober"
104106
require_relative "asrfacet_rb/scanner/probes/udp_prober"
105107
require_relative "asrfacet_rb/scanner/probes/icmp_prober"
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
# frozen_string_literal: true
2+
# For use only on systems you own or have explicit
3+
# written authorization to test.
4+
# SPDX-License-Identifier: Proprietary
5+
#
6+
# ASRFacet-Rb: Attack Surface Reconnaissance Framework
7+
# Copyright (c) 2026 voltsparx
8+
#
9+
# Author: voltsparx
10+
# Repository: https://github.com/voltsparx/ASRFacet-Rb
11+
# Contact: voltsparx@gmail.com
12+
# License: See LICENSE file in the project root
13+
#
14+
# This file is part of ASRFacet-Rb and is subject to the terms
15+
# and conditions defined in the LICENSE file.
16+
17+
require "open3"
18+
require "rbconfig"
19+
20+
module ASRFacet
21+
module Scanner
22+
module Platform
23+
module_function
24+
25+
def windows?
26+
host_os.match?(/mswin|mingw|cygwin/i)
27+
rescue StandardError
28+
false
29+
end
30+
31+
def macos?
32+
host_os.match?(/darwin/i)
33+
rescue StandardError
34+
false
35+
end
36+
37+
def linux?
38+
host_os.match?(/linux/i)
39+
rescue StandardError
40+
false
41+
end
42+
43+
def elevated?
44+
return windows_admin? if windows?
45+
return false unless Process.respond_to?(:uid)
46+
47+
Process.uid.zero?
48+
rescue StandardError
49+
false
50+
end
51+
52+
def command_available?(command_name)
53+
return false if command_name.to_s.strip.empty?
54+
55+
lookup = windows? ? ["where", command_name.to_s] : ["sh", "-lc", "command -v #{shell_escape(command_name.to_s)}"]
56+
_stdout, _stderr, status = Open3.capture3(*lookup)
57+
status.success?
58+
rescue StandardError
59+
false
60+
end
61+
62+
def nping_available?
63+
command_available?("nping")
64+
end
65+
66+
def sudo_available?
67+
!windows? && command_available?("sudo")
68+
end
69+
70+
def powershell_available?
71+
windows? && command_available?("powershell")
72+
end
73+
74+
def elevation_supported?
75+
return powershell_available? if windows?
76+
77+
sudo_available?
78+
rescue StandardError
79+
false
80+
end
81+
82+
def privilege_label
83+
return "Run as Administrator" if windows?
84+
85+
"sudo"
86+
rescue StandardError
87+
"elevated privileges"
88+
end
89+
90+
def host_label
91+
return "Windows" if windows?
92+
return "macOS" if macos?
93+
return "Linux" if linux?
94+
95+
host_os
96+
rescue StandardError
97+
"unknown"
98+
end
99+
100+
def raw_backend_requirements
101+
return "Nping with Npcap support and an elevated Administrator shell" if windows?
102+
103+
"Nping and an elevated shell such as sudo"
104+
rescue StandardError
105+
"a raw-capable backend and elevated privileges"
106+
end
107+
108+
def host_os
109+
RbConfig::CONFIG["host_os"].to_s
110+
rescue StandardError
111+
""
112+
end
113+
114+
def shell_escape(text)
115+
text.to_s.gsub("'", %q('\\'')) # shell-safe single-quote escape
116+
rescue StandardError
117+
text.to_s
118+
end
119+
120+
def windows_admin?
121+
script = "[Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()" \
122+
".IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)"
123+
stdout, _stderr, status = Open3.capture3("powershell", "-NoProfile", "-Command", script)
124+
status.success? && stdout.to_s.strip.casecmp("true").zero?
125+
rescue StandardError
126+
false
127+
end
128+
end
129+
end
130+
end

lib/asrfacet_rb/scanner/privilege.rb

Lines changed: 64 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,13 @@
1414
# This file is part of ASRFacet-Rb and is subject to the terms
1515
# and conditions defined in the LICENSE file.
1616

17+
require "rbconfig"
18+
1719
module ASRFacet
1820
module Scanner
1921
module Privilege
2022
RAW_SCAN_TYPES = %i[syn ack fin null xmas window maimon].freeze
23+
ELEVATION_MARKER = "ASRFACET_RB_ELEVATED_RELAUNCH".freeze
2124

2225
module_function
2326

@@ -28,10 +31,7 @@ def raw_scan_type?(scan_type)
2831
end
2932

3033
def elevated?
31-
return false if Gem.win_platform?
32-
return false unless Process.respond_to?(:uid)
33-
34-
Process.uid.zero?
34+
ASRFacet::Scanner::Platform.elevated?
3535
rescue StandardError
3636
false
3737
end
@@ -53,18 +53,75 @@ def validate!(scan_type:, tcp_prober:)
5353
true
5454
end
5555

56+
def maybe_relaunch!(scan_type:, tcp_prober:, argv:, requested: false)
57+
return false unless requested
58+
return false unless raw_scan_type?(scan_type)
59+
return false unless raw_socket_capable?(tcp_prober)
60+
return false if elevated?
61+
raise ASRFacet::ScanError, elevation_loop_message(scan_type) if ENV[ELEVATION_MARKER] == "1"
62+
raise ASRFacet::ScanError, unsupported_elevation_message(scan_type) unless ASRFacet::Scanner::Platform.elevation_supported?
63+
64+
relaunch_with_elevation!(Array(argv))
65+
true
66+
end
67+
5668
def unsupported_message(scan_type)
57-
"#{scan_type} scans need a raw-capable TCP prober and elevated privileges. " \
58-
"The bundled scanner backend is connect-oriented, so sudo alone will not make #{scan_type} behave like a real raw scan."
69+
"#{scan_type} scans need a raw-capable TCP prober backend such as Nping. " \
70+
"On #{ASRFacet::Scanner::Platform.host_label}, raw scans require #{ASRFacet::Scanner::Platform.raw_backend_requirements}."
5971
rescue StandardError
6072
"This scan type needs a raw-capable TCP prober and elevated privileges."
6173
end
6274

6375
def privilege_message(scan_type)
64-
"#{scan_type} scans need elevated privileges. Re-run as root or with sudo after providing a raw-capable TCP prober backend."
76+
"#{scan_type} scans need elevated privileges. Re-run with #{ASRFacet::Scanner::Platform.privilege_label} on #{ASRFacet::Scanner::Platform.host_label}, or pass --sudo so ASRFacet-Rb can relaunch itself."
6577
rescue StandardError
6678
"This scan type needs elevated privileges."
6779
end
80+
81+
def relaunch_with_elevation!(argv)
82+
return relaunch_windows!(argv) if ASRFacet::Scanner::Platform.windows?
83+
84+
relaunch_posix!(argv)
85+
end
86+
87+
def relaunch_posix!(argv)
88+
ENV[ELEVATION_MARKER] = "1"
89+
exec("sudo", "-E", RbConfig.ruby, File.expand_path($PROGRAM_NAME), *argv)
90+
rescue SystemCallError => e
91+
raise ASRFacet::ScanError, "Unable to relaunch with sudo: #{e.message}"
92+
end
93+
94+
def relaunch_windows!(argv)
95+
ENV[ELEVATION_MARKER] = "1"
96+
script_path = File.expand_path($PROGRAM_NAME)
97+
ruby_path = RbConfig.ruby
98+
ruby_literal = powershell_literal(ruby_path)
99+
argument_items = ([script_path] + Array(argv)).map { |item| powershell_literal(item) }.join(", ")
100+
command = "$proc = Start-Process -Verb RunAs -FilePath #{ruby_literal} -ArgumentList @(" \
101+
"#{argument_items}) -Wait -PassThru; exit $proc.ExitCode"
102+
system("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", command)
103+
exit($?.respond_to?(:exitstatus) ? $?.exitstatus.to_i : 0)
104+
rescue SystemCallError => e
105+
raise ASRFacet::ScanError, "Unable to relaunch with Administrator privileges: #{e.message}"
106+
end
107+
108+
def powershell_literal(text)
109+
"'#{text.to_s.gsub("'", "''")}'"
110+
rescue StandardError
111+
"''"
112+
end
113+
114+
def unsupported_elevation_message(scan_type)
115+
"#{scan_type} scans need elevation, but this host does not have an available #{ASRFacet::Scanner::Platform.privilege_label} workflow."
116+
rescue StandardError
117+
"This scan type needs elevation, but no supported elevation workflow is available."
118+
end
119+
120+
def elevation_loop_message(scan_type)
121+
"#{scan_type} scan relaunch already attempted, but the elevated process still does not have usable raw-scan privileges."
122+
rescue StandardError
123+
"An elevated relaunch was already attempted."
124+
end
68125
end
69126
end
70127
end

0 commit comments

Comments
 (0)