Skip to content

Commit 1538ec0

Browse files
committed
Stabalizatins
1 parent 6f453d6 commit 1538ec0

16 files changed

Lines changed: 918 additions & 100 deletions

lib/asrfacet_rb.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@
9898
require_relative "asrfacet_rb/scanner/results/scan_result"
9999
require_relative "asrfacet_rb/scanner/verbose_logger"
100100
require_relative "asrfacet_rb/scanner/probe_db"
101+
require_relative "asrfacet_rb/scanner/result_adapter"
101102
require_relative "asrfacet_rb/scanner/probes/tcp_prober"
102103
require_relative "asrfacet_rb/scanner/probes/udp_prober"
103104
require_relative "asrfacet_rb/scanner/probes/icmp_prober"
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
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 "resolv"
18+
19+
module ASRFacet
20+
module Scanner
21+
module ResultAdapter
22+
module_function
23+
24+
def to_payload(scan_result, target:)
25+
store = ASRFacet::ResultStore.new
26+
Array(scan_result&.host_results).each do |host_result|
27+
add_host_asset(store, host_result.host)
28+
Array(host_result.ports).each do |port_result|
29+
entry = {
30+
host: host_result.host,
31+
port: port_result.port,
32+
proto: port_result.proto,
33+
state: port_result.state,
34+
service: port_result.service,
35+
version: port_result.version,
36+
extra: port_result.extra,
37+
cpe: port_result.cpe,
38+
banner: port_result.banner,
39+
rtt: port_result.rtt,
40+
retries: port_result.retries
41+
}
42+
category = case port_result.state
43+
when :open then :open_ports
44+
when :closed then :closed_ports
45+
else :filtered_ports
46+
end
47+
store.add(category, entry)
48+
end
49+
end
50+
51+
summary = store.summary.merge(
52+
hosts_total: Array(scan_result&.host_results).size,
53+
hosts_up: Array(scan_result&.host_results).count(&:up),
54+
total_open: scan_result&.total_open.to_i,
55+
total_filtered: scan_result&.total_filtered.to_i,
56+
scan_type: scan_result&.scan_type.to_s
57+
)
58+
59+
{
60+
store: store,
61+
top_assets: [],
62+
summary: summary,
63+
scan_result: scan_result&.to_h,
64+
execution: { stages: [], failures: [], integrity: { status: "ok", summary: "Scanner run completed.", issues: [], recommendations: [] } },
65+
meta: { target: target.to_s }
66+
}
67+
end
68+
69+
def add_host_asset(store, host)
70+
if ip_address?(host)
71+
store.add(:ips, host)
72+
else
73+
store.add(:subdomains, host)
74+
end
75+
rescue StandardError
76+
nil
77+
end
78+
79+
def ip_address?(host)
80+
Resolv::IPv4::Regex.match?(host.to_s) || Resolv::IPv6::Regex.match?(host.to_s)
81+
rescue StandardError
82+
false
83+
end
84+
end
85+
end
86+
end

lib/asrfacet_rb/ui/cli.rb

Lines changed: 19 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -182,19 +182,20 @@ class << self
182182
def start(given_args = ARGV, config = {})
183183
args = Array(given_args).dup
184184
ASRFacet::UI::FirstRunGuide.maybe_print(args)
185-
if args.delete("--console") || args.delete("-C")
185+
explicit_command = args.first.to_s.match?(/\A[^-]/)
186+
if !explicit_command && (args.delete("--console") || args.delete("-C"))
186187
return super(["console", *args], config)
187188
end
188-
if args.delete("--web-session")
189+
if !explicit_command && args.delete("--web-session")
189190
return super(["web", *args], config)
190191
end
191-
if args.delete("--about")
192+
if !explicit_command && args.delete("--about")
192193
return super(["about", *args], config)
193194
end
194-
if args.delete("--version")
195+
if !explicit_command && args.delete("--version")
195196
return super(["version", *args], config)
196197
end
197-
if (index = args.index("--explain"))
198+
if !explicit_command && (index = args.index("--explain"))
198199
topic = args[index + 1].to_s
199200
args.slice!(index, 2)
200201
return super(["explain", topic, *args], config)
@@ -258,13 +259,16 @@ def passive(domain)
258259
def ports(host)
259260
return unless ensure_framework_ready!
260261

261-
store = ASRFacet::ResultStore.new
262-
ASRFacet::Core::ThreadSafe.print_status("Starting focused port discovery against #{host}") if options[:verbose]
263-
ASRFacet::Engines::PortEngine.new.scan(host, options[:ports] || "top100", workers: options[:threads]).each do |entry|
264-
store.add(:open_ports, entry)
265-
announce_event(:open_port, entry.merge(host: host)) if options[:verbose]
266-
end
267-
output_results({ store: store, top_assets: [], summary: store.summary }, host)
262+
scan_result = ASRFacet::Scanner::ScanEngine.new(
263+
scan_type: "connect",
264+
timing: 3,
265+
verbosity: options[:verbose] ? 1 : 0,
266+
version_detection: false,
267+
os_detection: false,
268+
version_intensity: 7,
269+
ports: options[:ports] || "top100"
270+
).scan(host)
271+
output_results(ASRFacet::Scanner::ResultAdapter.to_payload(scan_result, target: host), host)
268272
rescue ASRFacet::Error => e
269273
report_exception("ports", e)
270274
end
@@ -280,23 +284,16 @@ def ports(host)
280284
def portscan(target)
281285
return unless ensure_framework_ready!
282286

283-
engine = ASRFacet::Scanner::ScanEngine.new(
287+
scan_result = ASRFacet::Scanner::ScanEngine.new(
284288
scan_type: options[:type],
285289
timing: options[:timing],
286290
verbosity: options[:verbosity],
287291
version_detection: options[:version],
288292
os_detection: options[:os],
289293
version_intensity: options[:intensity],
290294
ports: options[:ports]
291-
)
292-
result = engine.scan(target)
293-
payload = JSON.pretty_generate(result.to_h)
294-
if options[:output].to_s.empty?
295-
puts(payload) if options[:format].to_s == "json"
296-
else
297-
File.write(options[:output], payload)
298-
puts(options[:output])
299-
end
295+
).scan(target)
296+
output_results(ASRFacet::Scanner::ResultAdapter.to_payload(scan_result, target: target), target)
300297
rescue ASRFacet::Error => e
301298
report_exception("portscan", e)
302299
end

lib/asrfacet_rb/ui/help_catalog.rb

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,26 @@ module HelpCatalog
4747
summary: "Run a focused TCP port scan against a host or IP.",
4848
usage: "asrfacet-rb ports HOST [--ports top100|top1000|1-1000|80,443]",
4949
details: [
50-
"Use this command when you only need network exposure and service banners.",
50+
"Use this command when you only need a quick connectivity pass through the scanner engine without version or OS fingerprinting.",
5151
"The port selector accepts named ranges, numeric ranges, or comma-separated custom lists."
5252
],
5353
examples: [
5454
"asrfacet-rb ports 192.0.2.10",
5555
"asrfacet-rb ports app.example.com --ports 22,80,443,8080"
5656
]
5757
},
58+
"portscan" => {
59+
summary: "Run the full scanner engine directly with explicit scan type, timing, and optional fingerprinting.",
60+
usage: "asrfacet-rb portscan TARGET --type connect|syn|udp|ack|fin|null|xmas|window|maimon|ping|service [--timing 0-5] [--version] [--os]",
61+
details: [
62+
"Use this command when you want direct control over the scanner engine instead of the simpler `ports` wrapper.",
63+
"Timing templates mirror the scanner timing profiles and the command can also render PDF, DOCX, CSV, JSON, HTML, TXT, CLI, ALL, or SARIF output."
64+
],
65+
examples: [
66+
"asrfacet-rb portscan 192.0.2.10 --type syn --timing 4 --ports 1-1024",
67+
"asrfacet-rb portscan app.example.com --type service --version --intensity 9 --format json"
68+
]
69+
},
5870
"dns" => {
5971
summary: "Collect DNS records for a domain without running the full pipeline.",
6072
usage: "asrfacet-rb dns DOMAIN [--format cli|json|html|txt]",
@@ -121,7 +133,7 @@ module HelpCatalog
121133
details: [
122134
"Web session mode starts a local-only dashboard for recon planning, saved sessions, run history, report browsing, and live stage updates.",
123135
"Session drafts are autosaved to disk so configuration survives accidental browser closes, process crashes, and power loss.",
124-
"The dashboard uses the same pipeline, memory, monitoring, headless, webhook, and rate-control options as the CLI."
136+
"The dashboard uses the same pipeline, scanner engine, memory, monitoring, headless, webhook, rate-control, and multi-format reporting options as the CLI."
125137
],
126138
examples: [
127139
"asrfacet-rb --web-session",
@@ -133,13 +145,14 @@ module HelpCatalog
133145
usage: "--format cli|json|html|txt and --output PATH",
134146
details: [
135147
"CLI output prints directly to the terminal and is best for quick inspection.",
136-
"JSON is best for scripting, HTML is best for sharing a styled offline report, and TXT is a plain-text export.",
148+
"JSON and SARIF are best for scripting, HTML and PDF are best for polished sharing, DOCX is best for editable handoff, TXT is a plain-text export, and CSV creates flat data slices.",
137149
"ASRFacet-Rb also stores a full report bundle automatically under ~/.asrfacet_rb/output/reports/<target>/<timestamp>/ so installed users can find earlier runs easily.",
138-
"Use `--output` when you want an additional custom file path alongside the stored report bundle."
150+
"Use `--output` when you want an additional custom file path alongside the stored report bundle. Use `--format all` when you want the framework to emit every supported report flavor in one request."
139151
],
140152
examples: [
141153
"asrfacet-rb scan example.com --format html --output report.html",
142-
"asrfacet-rb passive example.com --format json --output passive.json"
154+
"asrfacet-rb passive example.com --format json --output passive.json",
155+
"asrfacet-rb portscan 192.0.2.10 --format pdf --output service-map.pdf"
143156
]
144157
},
145158
"scope" => {
@@ -465,6 +478,7 @@ def menu(executable: PRIMARY_EXECUTABLE)
465478
" scan DOMAIN Full reconnaissance pipeline Aliases: s, sc",
466479
" passive DOMAIN Passive subdomain discovery only Aliases: p, pa",
467480
" ports HOST Focused TCP port scan Aliases: pt, po",
481+
" portscan TARGET Direct scanner engine control Aliases: none",
468482
" dns DOMAIN DNS record collection only Aliases: d, dn",
469483
" lab Start the local validation lab Aliases: none",
470484
" interactive Guided beginner workflow Aliases: i, int",
@@ -478,7 +492,7 @@ def menu(executable: PRIMARY_EXECUTABLE)
478492
"",
479493
"Global options:",
480494
" -o, --output PATH Save output to a file instead of printing",
481-
" -f, --format TYPE cli, json, html, or txt",
495+
" -f, --format TYPE cli, json, html, txt, csv, pdf, docx, all, or sarif",
482496
" -v, --verbose Print stage-by-stage status messages",
483497
" -t, --threads N Worker concurrency for threaded engines",
484498
" --timeout SEC Network timeout for active requests",
@@ -503,6 +517,7 @@ def menu(executable: PRIMARY_EXECUTABLE)
503517
" #{executable} scan example.com --ports top1000 --format html --output report.html",
504518
" #{executable} passive example.com --format json",
505519
" #{executable} ports api.example.com --ports 80,443,8443",
520+
" #{executable} portscan 192.0.2.10 --type syn --timing 4 --ports 1-1024",
506521
" #{executable} lab",
507522
" #{executable} about",
508523
" #{executable} help scan",

lib/asrfacet_rb/ui/interactive.rb

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
# frozen_string_literal: true
2+
# For use only on systems you own or have explicit
3+
# written authorization to test.
24
# SPDX-License-Identifier: Proprietary
35
#
46
# ASRFacet-Rb: Attack Surface Reconnaissance Framework
@@ -25,25 +27,48 @@ def initialize(prompt: TTY::Prompt.new)
2527

2628
def start
2729
target = @prompt.ask("Target domain:") { |q| q.required(true) }
28-
mode = @prompt.select("Scan mode:", ["Full", "Passive", "Ports", "DNS"])
30+
mode = @prompt.select("Scan mode:", ["Full", "Passive", "Ports", "Portscan", "DNS"])
2931
port_range = nil
30-
if %w[Full Ports].include?(mode)
32+
scan_type = "connect"
33+
timing = 3
34+
version_detection = false
35+
os_detection = false
36+
intensity = 7
37+
if %w[Full Ports Portscan].include?(mode)
3138
port_choice = @prompt.select("Port range:", ["Top100", "Top1000", "Custom"])
3239
port_range = port_choice == "Custom" ? @prompt.ask("Custom port range:") : port_choice.downcase
3340
end
41+
if mode == "Portscan"
42+
scan_type = @prompt.select("Scan type:", %w[connect syn udp ack fin null xmas window maimon ping service])
43+
timing = @prompt.select("Timing template:", (0..5).to_a)
44+
version_detection = @prompt.yes?("Enable version detection?")
45+
os_detection = @prompt.yes?("Enable OS detection?")
46+
intensity = @prompt.select("Version intensity:", (0..9).to_a) if version_detection
47+
end
3448
output_format = @prompt.select("Output format:", ["CLI", "JSON", "HTML", "TXT"]).downcase
3549
shodan_key = @prompt.yes?("Add a Shodan key?") ? @prompt.mask("Shodan API key:") : nil
3650

3751
summary = [
3852
"Target: #{target}",
3953
"Mode: #{mode}",
4054
"Ports: #{port_range || 'n/a'}",
55+
"Scan type: #{mode == 'Portscan' ? scan_type : 'n/a'}",
4156
"Format: #{output_format}",
4257
"Shodan: #{shodan_key.to_s.empty? ? 'no' : 'yes'}"
4358
].join(" | ")
4459
return nil unless @prompt.yes?("Run scan? #{summary}")
4560

46-
result = run_with_spinners(target, mode, port_range, shodan_key)
61+
result = run_with_spinners(
62+
target,
63+
mode,
64+
port_range,
65+
shodan_key,
66+
scan_type: scan_type,
67+
timing: timing,
68+
version_detection: version_detection,
69+
os_detection: os_detection,
70+
intensity: intensity
71+
)
4772
render_output(result, output_format)
4873
rescue StandardError => e
4974
ASRFacet::Core::ThreadSafe.print_error(e.message)
@@ -52,7 +77,7 @@ def start
5277

5378
private
5479

55-
def run_with_spinners(target, mode, port_range, shodan_key)
80+
def run_with_spinners(target, mode, port_range, shodan_key, scan_type:, timing:, version_detection:, os_detection:, intensity:)
5681
case mode
5782
when "Full"
5883
dashboard = ASRFacet::ProgressDashboard.new
@@ -81,12 +106,30 @@ def run_with_spinners(target, mode, port_range, shodan_key)
81106
{ store: store, top_assets: [] }
82107
when "Ports"
83108
ASRFacet::Core::ThreadSafe.print_status("Running port scan")
84-
store = ASRFacet::ResultStore.new
85-
ASRFacet::Engines::PortEngine.new.scan(target, port_range || "top100").each do |entry|
86-
store.add(:open_ports, entry)
87-
end
109+
scan_result = ASRFacet::Scanner::ScanEngine.new(
110+
scan_type: :connect,
111+
timing: 3,
112+
verbosity: 0,
113+
version_detection: false,
114+
os_detection: false,
115+
version_intensity: 7,
116+
ports: port_range || "top100"
117+
).scan(target)
88118
ASRFacet::Core::ThreadSafe.print_good("Port scan complete")
89-
{ store: store, top_assets: [] }
119+
ASRFacet::Scanner::ResultAdapter.to_payload(scan_result, target: target)
120+
when "Portscan"
121+
ASRFacet::Core::ThreadSafe.print_status("Running scanner engine")
122+
scan_result = ASRFacet::Scanner::ScanEngine.new(
123+
scan_type: scan_type,
124+
timing: timing,
125+
verbosity: 0,
126+
version_detection: version_detection,
127+
os_detection: os_detection,
128+
version_intensity: intensity,
129+
ports: port_range || "top100"
130+
).scan(target)
131+
ASRFacet::Core::ThreadSafe.print_good("Scanner engine complete")
132+
ASRFacet::Scanner::ResultAdapter.to_payload(scan_result, target: target)
90133
else
91134
ASRFacet::Core::ThreadSafe.print_status("Collecting DNS records")
92135
store = ASRFacet::ResultStore.new

0 commit comments

Comments
 (0)