Skip to content

Commit 7590049

Browse files
committed
Updates
1 parent 4ce95f5 commit 7590049

9 files changed

Lines changed: 344 additions & 0 deletions

File tree

lib/asrfacet_rb/scanner/probe_db.rb

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,23 @@ def to_h
4444
ROOT = File.expand_path("../../../temp/nmap", __dir__)
4545
SERVICES_PATH = File.join(ROOT, "nmap-services")
4646
PROBES_PATH = File.join(ROOT, "nmap-service-probes")
47+
SERVICE_FAMILY_ALIASES = {
48+
"null" => { probe_names: ["NULL"], services: [] },
49+
"genericlines" => { probe_names: ["GenericLines"], services: [] },
50+
"httpoptions" => { probe_names: ["HTTPOptions"], services: ["http"] },
51+
"rtsprequest" => { probe_names: ["RTSPRequest"], services: ["rtsp"] },
52+
"sslsessionreq" => { probe_names: ["SSLSessionReq"], services: ["ssl", "https"] },
53+
"sshsessionreq" => { probe_names: [], services: ["ssh"] },
54+
"smtprequest" => { probe_names: [], services: ["smtp"] },
55+
"ftprequest" => { probe_names: [], services: ["ftp"] },
56+
"mssqlquery" => { probe_names: ["Sqlping"], services: ["ms-sql-s"] },
57+
"mysqlrequest" => { probe_names: [], services: ["mysql", "mysqlx"] },
58+
"postgresrequest" => { probe_names: [], services: ["postgresql"] },
59+
"redisrequest" => { probe_names: ["redis-server"], services: ["redis"] },
60+
"mongodbrequest" => { probe_names: ["mongodb"], services: ["mongodb"] },
61+
"dnsquery" => { probe_names: ["DNSVersionBindReq", "DNSVersionBindReqTCP", "DNSStatusRequest", "DNSStatusRequestTCP", "DNS-SD", "DNS-SD-TCP", "DNS_SD_QU"], services: ["domain", "mdns"] },
62+
"sipoptions" => { probe_names: ["SIPOptions"], services: ["sip"] }
63+
}.freeze
4764

4865
class << self
4966
private
@@ -238,6 +255,31 @@ def service_for(port, proto)
238255
def top_ports(count)
239256
TOP_PORTS.first(count.to_i)
240257
end
258+
259+
def probes_for_service(service, proto: nil)
260+
family = SERVICE_FAMILY_ALIASES.fetch(service.to_s.strip.downcase, {
261+
probe_names: [service.to_s],
262+
services: [service.to_s.downcase]
263+
})
264+
PROBES.select do |probe|
265+
next false if proto && probe.proto != proto.to_sym
266+
267+
family[:probe_names].include?(probe.name) || service_match?(probe, family[:services])
268+
end
269+
end
270+
271+
def supports_service?(service, proto: nil)
272+
!probes_for_service(service, proto: proto).empty?
273+
end
274+
275+
private
276+
277+
def service_match?(probe, services)
278+
return false if services.empty?
279+
280+
all_services = probe.matches.map { |entry| entry[:service] } + probe.softmatches.map { |entry| entry[:service] }
281+
services.any? { |service| all_services.include?(service) }
282+
end
241283
end
242284
end
243285
end

spec/scanner/probe_db_spec.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,17 @@
3434
it "looks up a service name for a known TCP port" do
3535
expect(probe_db.service_for(22, :tcp)).to eq("ssh")
3636
end
37+
38+
it "supports the requested source-derived service families" do
39+
expect(probe_db.supports_service?("SSHSessionReq", proto: :tcp)).to be(true)
40+
expect(probe_db.supports_service?("SMTPRequest", proto: :tcp)).to be(true)
41+
expect(probe_db.supports_service?("FTPRequest", proto: :tcp)).to be(true)
42+
expect(probe_db.supports_service?("MSSQLQuery", proto: :udp)).to be(true)
43+
expect(probe_db.supports_service?("MySQLRequest", proto: :tcp)).to be(true)
44+
expect(probe_db.supports_service?("PostgresRequest", proto: :tcp)).to be(true)
45+
expect(probe_db.supports_service?("RedisRequest", proto: :tcp)).to be(true)
46+
expect(probe_db.supports_service?("MongoDBRequest", proto: :tcp)).to be(true)
47+
expect(probe_db.supports_service?("DNSQuery")).to be(true)
48+
expect(probe_db.supports_service?("SIPOptions")).to be(true)
49+
end
3750
end
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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 "spec_helper"
18+
19+
RSpec.describe ASRFacet::Scanner::ScanTypes::AckScan do
20+
let(:probe_db) { instance_double(ASRFacet::Scanner::ProbeDB, service_for: "http") }
21+
let(:timing) { ASRFacet::Scanner::Timing.get(3) }
22+
23+
it "maps an RST reply to unfiltered" do
24+
context = instance_double(ASRFacet::Scanner::ScanContext, timing: timing, probe_db: probe_db, tcp_prober: instance_double(ASRFacet::Scanner::Probes::TCPProber, send_probe: { reply: :rst, window: 0 }))
25+
26+
result = described_class.new(context).probe("example.com", 80)
27+
28+
expect(result.state).to eq(:unfiltered)
29+
end
30+
31+
it "maps no reply to filtered" do
32+
context = instance_double(ASRFacet::Scanner::ScanContext, timing: timing, probe_db: probe_db, tcp_prober: instance_double(ASRFacet::Scanner::Probes::TCPProber, send_probe: { reply: :timeout, window: 0 }))
33+
34+
result = described_class.new(context).probe("example.com", 80)
35+
36+
expect(result.state).to eq(:filtered)
37+
end
38+
end
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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 "spec_helper"
18+
19+
RSpec.describe ASRFacet::Scanner::ScanTypes::FinScan do
20+
let(:probe_db) { instance_double(ASRFacet::Scanner::ProbeDB, service_for: "http") }
21+
let(:timing) { ASRFacet::Scanner::Timing.get(3) }
22+
23+
it "maps an RST reply to closed" do
24+
context = instance_double(ASRFacet::Scanner::ScanContext, timing: timing, probe_db: probe_db, tcp_prober: instance_double(ASRFacet::Scanner::Probes::TCPProber, send_probe: { reply: :rst, window: 0 }))
25+
26+
result = described_class.new(context).probe("example.com", 80)
27+
28+
expect(result.state).to eq(:closed)
29+
end
30+
31+
it "maps no reply to open_filtered" do
32+
context = instance_double(ASRFacet::Scanner::ScanContext, timing: timing, probe_db: probe_db, tcp_prober: instance_double(ASRFacet::Scanner::Probes::TCPProber, send_probe: { reply: :timeout, window: 0 }))
33+
34+
result = described_class.new(context).probe("example.com", 80)
35+
36+
expect(result.state).to eq(:open_filtered)
37+
end
38+
end
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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 "spec_helper"
18+
19+
RSpec.describe ASRFacet::Scanner::ScanTypes::MaimonScan do
20+
let(:probe_db) { instance_double(ASRFacet::Scanner::ProbeDB, service_for: "http") }
21+
let(:timing) { ASRFacet::Scanner::Timing.get(3) }
22+
23+
it "maps an RST reply to closed" do
24+
context = instance_double(ASRFacet::Scanner::ScanContext, timing: timing, probe_db: probe_db, tcp_prober: instance_double(ASRFacet::Scanner::Probes::TCPProber, send_probe: { reply: :rst, window: 0 }))
25+
26+
result = described_class.new(context).probe("example.com", 80)
27+
28+
expect(result.state).to eq(:closed)
29+
end
30+
31+
it "maps no reply to open_filtered" do
32+
context = instance_double(ASRFacet::Scanner::ScanContext, timing: timing, probe_db: probe_db, tcp_prober: instance_double(ASRFacet::Scanner::Probes::TCPProber, send_probe: { reply: :timeout, window: 0 }))
33+
34+
result = described_class.new(context).probe("example.com", 80)
35+
36+
expect(result.state).to eq(:open_filtered)
37+
end
38+
end
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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 "spec_helper"
18+
19+
RSpec.describe ASRFacet::Scanner::ScanTypes::NullScan do
20+
let(:probe_db) { instance_double(ASRFacet::Scanner::ProbeDB, service_for: "http") }
21+
let(:timing) { ASRFacet::Scanner::Timing.get(3) }
22+
23+
it "maps an RST reply to closed" do
24+
context = instance_double(ASRFacet::Scanner::ScanContext, timing: timing, probe_db: probe_db, tcp_prober: instance_double(ASRFacet::Scanner::Probes::TCPProber, send_probe: { reply: :rst, window: 0 }))
25+
26+
result = described_class.new(context).probe("example.com", 80)
27+
28+
expect(result.state).to eq(:closed)
29+
end
30+
31+
it "maps no reply to open_filtered" do
32+
context = instance_double(ASRFacet::Scanner::ScanContext, timing: timing, probe_db: probe_db, tcp_prober: instance_double(ASRFacet::Scanner::Probes::TCPProber, send_probe: { reply: :timeout, window: 0 }))
33+
34+
result = described_class.new(context).probe("example.com", 80)
35+
36+
expect(result.state).to eq(:open_filtered)
37+
end
38+
end
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
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 "spec_helper"
18+
19+
RSpec.describe ASRFacet::Scanner::ScanTypes::ServiceScan do
20+
let(:probe_db) { instance_double(ASRFacet::Scanner::ProbeDB, service_for: "http") }
21+
let(:version_detector) { instance_double(ASRFacet::Scanner::VersionDetector) }
22+
let(:context) do
23+
instance_double(
24+
ASRFacet::Scanner::ScanContext,
25+
timing: ASRFacet::Scanner::Timing.get(3),
26+
probe_db: probe_db,
27+
version_detector: version_detector
28+
)
29+
end
30+
31+
it "annotates an open TCP port with version metadata" do
32+
socket = instance_double(TCPSocket, close: true)
33+
socket_class = class_double(TCPSocket, new: socket)
34+
allow(version_detector).to receive(:detect).and_return(service: "http", version: "Apache 2.4.57", extra: "server", cpe: "cpe:/a:apache:http_server:2.4.57", banner: "HTTP/1.1 200 OK")
35+
36+
result = described_class.new(context, socket_class: socket_class).probe("example.com", 80)
37+
38+
expect(result.state).to eq(:open)
39+
expect(result.version).to eq("Apache 2.4.57")
40+
expect(result.cpe).to eq("cpe:/a:apache:http_server:2.4.57")
41+
end
42+
43+
it "does not attempt version detection for a closed port" do
44+
socket_class = class_double(TCPSocket)
45+
allow(socket_class).to receive(:new).and_raise(Errno::ECONNREFUSED)
46+
allow(version_detector).to receive(:detect)
47+
48+
result = described_class.new(context, socket_class: socket_class).probe("example.com", 80)
49+
50+
expect(result.state).to eq(:closed)
51+
expect(version_detector).not_to have_received(:detect)
52+
end
53+
end
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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 "spec_helper"
18+
19+
RSpec.describe ASRFacet::Scanner::ScanTypes::WindowScan do
20+
let(:probe_db) { instance_double(ASRFacet::Scanner::ProbeDB, service_for: "http") }
21+
let(:timing) { ASRFacet::Scanner::Timing.get(3) }
22+
23+
it "maps an RST with a positive window to open" do
24+
context = instance_double(ASRFacet::Scanner::ScanContext, timing: timing, probe_db: probe_db, tcp_prober: instance_double(ASRFacet::Scanner::Probes::TCPProber, send_probe: { reply: :rst, window: 1024 }))
25+
26+
result = described_class.new(context).probe("example.com", 80)
27+
28+
expect(result.state).to eq(:open)
29+
end
30+
31+
it "maps an RST with a zero window to closed" do
32+
context = instance_double(ASRFacet::Scanner::ScanContext, timing: timing, probe_db: probe_db, tcp_prober: instance_double(ASRFacet::Scanner::Probes::TCPProber, send_probe: { reply: :rst, window: 0 }))
33+
34+
result = described_class.new(context).probe("example.com", 80)
35+
36+
expect(result.state).to eq(:closed)
37+
end
38+
39+
it "maps no reply to filtered" do
40+
context = instance_double(ASRFacet::Scanner::ScanContext, timing: timing, probe_db: probe_db, tcp_prober: instance_double(ASRFacet::Scanner::Probes::TCPProber, send_probe: { reply: :timeout, window: 0 }))
41+
42+
result = described_class.new(context).probe("example.com", 80)
43+
44+
expect(result.state).to eq(:filtered)
45+
end
46+
end
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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 "spec_helper"
18+
19+
RSpec.describe ASRFacet::Scanner::ScanTypes::XmasScan do
20+
let(:probe_db) { instance_double(ASRFacet::Scanner::ProbeDB, service_for: "http") }
21+
let(:timing) { ASRFacet::Scanner::Timing.get(3) }
22+
23+
it "maps an RST reply to closed" do
24+
context = instance_double(ASRFacet::Scanner::ScanContext, timing: timing, probe_db: probe_db, tcp_prober: instance_double(ASRFacet::Scanner::Probes::TCPProber, send_probe: { reply: :rst, window: 0 }))
25+
26+
result = described_class.new(context).probe("example.com", 80)
27+
28+
expect(result.state).to eq(:closed)
29+
end
30+
31+
it "maps no reply to open_filtered" do
32+
context = instance_double(ASRFacet::Scanner::ScanContext, timing: timing, probe_db: probe_db, tcp_prober: instance_double(ASRFacet::Scanner::Probes::TCPProber, send_probe: { reply: :timeout, window: 0 }))
33+
34+
result = described_class.new(context).probe("example.com", 80)
35+
36+
expect(result.state).to eq(:open_filtered)
37+
end
38+
end

0 commit comments

Comments
 (0)