Skip to content

Commit 4ab9236

Browse files
committed
Added the ronin-nmap grep command (closes #3).
1 parent 8bb6977 commit 4ab9236

File tree

6 files changed

+1653
-1
lines changed

6 files changed

+1653
-1
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ Commands:
4040
completion
4141
convert
4242
dump
43+
grep
4344
help
4445
import
4546
print

gemspec.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ generated_files:
2424
- man/ronin-nmap-completion.1
2525
- man/ronin-nmap-convert.1
2626
- man/ronin-nmap-dump.1
27+
- man/ronin-nmap-grep.1
2728
- man/ronin-nmap-import.1
2829
- man/ronin-nmap-print.1
2930
- man/ronin-nmap-scan.1

lib/ronin/nmap/cli/commands/grep.rb

Lines changed: 378 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,378 @@
1+
# frozen_string_literal: true
2+
#
3+
# ronin-nmap - A Ruby library for automating nmap and importing nmap scans.
4+
#
5+
# Copyright (c) 2023 Hal Brodigan ([email protected])
6+
#
7+
# ronin-nmap is free software: you can redistribute it and/or modify
8+
# it under the terms of the GNU Lesser General Public License as published
9+
# by the Free Software Foundation, either version 3 of the License, or
10+
# (at your option) any later version.
11+
#
12+
# ronin-nmap is distributed in the hope that it will be useful,
13+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
# GNU Lesser General Public License for more details.
16+
#
17+
# You should have received a copy of the GNU Lesser General Public License
18+
# along with ronin-nmap. If not, see <https://www.gnu.org/licenses/>.
19+
#
20+
21+
require 'ronin/nmap/cli/command'
22+
23+
require 'command_kit/colors'
24+
require 'command_kit/printing/indent'
25+
require 'nmap/xml'
26+
27+
module Ronin
28+
module Nmap
29+
class CLI
30+
module Commands
31+
#
32+
# Parses and searches nmap XML file(s) for the pattern.
33+
#
34+
# ## Usage
35+
#
36+
# ronin-nmap grep [options] PATTERN XML_FILE [...]
37+
#
38+
# ## Options
39+
#
40+
# -h, --help Print help information
41+
#
42+
# ## Arguments
43+
#
44+
class Grep < Command
45+
46+
include CommandKit::Colors
47+
include CommandKit::Printing::Indent
48+
49+
usage '[options] PATTERN XML_FILE [...]'
50+
51+
argument :pattern, required: true,
52+
desc: 'The pattern to search for'
53+
54+
argument :xml_file, required: true,
55+
repeats: true,
56+
desc: 'The nmap XML file to search'
57+
58+
description 'Parses and searches nmap XML file(s) for the pattern'
59+
60+
man_page 'ronin-nmap-grep.1'
61+
62+
#
63+
# Runs the `ronin-nmap grep` command.
64+
#
65+
# @param [String] pattern
66+
# The pattern to search for.
67+
#
68+
# @param [Array<String>] xml_files
69+
# The nmap `.xml` file(s) to grep.
70+
#
71+
def run(pattern,*xml_files)
72+
xml_files.each do |xml_file|
73+
unless File.file?(xml_file)
74+
print_error "no such file or directory: #{xml_file}"
75+
next
76+
end
77+
78+
xml = ::Nmap::XML.open(xml_file)
79+
hosts = grep_xml(xml,pattern)
80+
81+
highlight_hosts(hosts,pattern)
82+
end
83+
end
84+
85+
#
86+
# Searches the parsed nmap XML for the text pattern.
87+
#
88+
# @param [::Nmap::XML] xml
89+
# The parsed nmap XML object.
90+
#
91+
# @param [String] pattern
92+
# The text pattern to search for.
93+
#
94+
# @return [Enumerator::Lazy<::Nmap::XML::Host>]
95+
# The nmap XML host objects that contain the text pattern.
96+
#
97+
def grep_xml(xml,pattern)
98+
xml.each_up_host.lazy.filter do |host|
99+
match_host(host,pattern)
100+
end
101+
end
102+
103+
#
104+
# Determines if the nmap XML host object contains the text pattern.
105+
#
106+
# @param [::Nmap::XML::Host] host
107+
# The nmap XML host object to search.
108+
#
109+
# @param [String] pattern
110+
# The text pattern to search for.
111+
#
112+
# @return [Boolean]
113+
#
114+
def match_host(host,pattern)
115+
hostnames = host.each_hostname
116+
open_ports = host.each_open_port
117+
host_script = host.host_script
118+
119+
hostnames.any? { |hostname| match_hostname(hostname,pattern) } ||
120+
open_ports.any? { |port| match_port(port,pattern) } ||
121+
(host_script && match_scripts(host_script,pattern))
122+
end
123+
124+
#
125+
# Determines if the nmap XML hostname object contains the text
126+
# pattern.
127+
#
128+
# @param [::Nmap::XML::Hostname] hostname
129+
# The nmap XML hostname object to search.
130+
#
131+
# @param [String] pattern
132+
# The text pattern to search for.
133+
#
134+
# @return [Boolean]
135+
#
136+
def match_hostname(hostname,pattern)
137+
hostname.name.match(pattern)
138+
end
139+
140+
#
141+
# Determines if the nmap XML port object contains the text pattern.
142+
#
143+
# @param [::Nmap::XML::Port] port
144+
# The nmap XML port object to search.
145+
#
146+
# @param [String] pattern
147+
# The text pattern to search for.
148+
#
149+
# @return [Boolean]
150+
#
151+
def match_port(port,pattern)
152+
match_scripts(port,pattern) || if (service = port.service)
153+
match_service(service,pattern)
154+
end
155+
end
156+
157+
#
158+
# Determines if the nmap XML service object contains the text pattern.
159+
#
160+
# @param [::Nmap::XML::Service] service
161+
# The nmap XML service object to search.
162+
#
163+
# @param [String] pattern
164+
# The text pattern to search for.
165+
#
166+
# @return [Boolean]
167+
#
168+
def match_service(service,pattern)
169+
product = service.product
170+
version = service.version
171+
extra_info = service.extra_info
172+
173+
service.name.match(pattern) ||
174+
(product && product.match(pattern)) ||
175+
(version && version.match(pattern)) ||
176+
(extra_info && extra_info.match(pattern))
177+
end
178+
179+
#
180+
# Determines if the nmap XML scripts object contains the text pattern.
181+
#
182+
# @param [::Nmap::XML::Scripts] has_scripts
183+
# The nmap XML object that includes `Nmap::XML::Scripts`.
184+
#
185+
# @param [String] pattern
186+
# The text pattern to search for.
187+
#
188+
# @return [Boolean]
189+
#
190+
def match_scripts(has_scripts,pattern)
191+
has_scripts.scripts.any? do |id,script|
192+
match_script(script,pattern)
193+
end
194+
end
195+
196+
#
197+
# Determines if the nmap XML script object contains the text pattern.
198+
#
199+
# @param [::Nmap::XML::Script] script
200+
# The nmap XML script object to search.
201+
#
202+
# @param [String] pattern
203+
# The text pattern to search for.
204+
#
205+
# @return [Boolean]
206+
#
207+
def match_script(script,pattern)
208+
script.id.match(pattern) || script.output.match(pattern)
209+
end
210+
211+
#
212+
# Prints the nmap hosts with the pattern highlighted in the output.
213+
#
214+
# @param [Enumerator::Lazy<::Nmap::XML::Host>] hosts
215+
# The nmap hosts to print.
216+
#
217+
# @param [String] pattern
218+
# The pattern to highlight in the output.
219+
#
220+
def highlight_hosts(hosts,pattern)
221+
hosts.each do |host|
222+
highlight_host(host,pattern)
223+
puts
224+
end
225+
end
226+
227+
#
228+
# Prints the nmap host with the pattern highlighted in the output.
229+
#
230+
# @param [::Nmap::XML::Host] host
231+
# The nmap host to print.
232+
#
233+
# @param [String] pattern
234+
# The text pattern to highlight in the output.
235+
#
236+
def highlight_host(host,pattern)
237+
addresses = host.addresses
238+
hostnames = host.hostnames
239+
240+
unless hostnames.empty?
241+
puts "[ #{addresses.first} / #{highlight(hostnames.first,pattern)} ]"
242+
else
243+
puts "[ #{addresses.first} ]"
244+
end
245+
puts
246+
247+
indent do
248+
if addresses.length > 1
249+
puts "[ addresses ]"
250+
puts
251+
252+
indent do
253+
addresses.each do |address|
254+
puts address
255+
end
256+
end
257+
puts
258+
end
259+
260+
if hostnames.length > 1
261+
puts "[ hostnames ]"
262+
puts
263+
264+
indent do
265+
hostnames.each do |hostname|
266+
puts highlight(hostname,pattern)
267+
end
268+
end
269+
puts
270+
end
271+
272+
if (host_script = host.host_script)
273+
puts "[ host scripts ]"
274+
puts
275+
276+
indent do
277+
highlight_scripts(host_script)
278+
end
279+
end
280+
281+
puts "[ ports ]"
282+
puts
283+
284+
indent do
285+
host.each_open_port do |port|
286+
highlight_port(port,pattern)
287+
end
288+
end
289+
end
290+
end
291+
292+
#
293+
# Prints the nmap port with the pattern highlighted in the output.
294+
#
295+
# @param [::Nmap::XML::Port] port
296+
# The nmap XML port object to print.
297+
#
298+
# @param [String] pattern
299+
# The text pattern to highlight in the output.
300+
#
301+
def highlight_port(port,pattern)
302+
port_line = "#{port.number}/#{port.protocol}\t#{port.state}"
303+
304+
if (service = port.service)
305+
port_line << "\t#{highlight(service,pattern)}"
306+
307+
if (extra_info = service.extra_info)
308+
port_line << " #{highlight(extra_info,pattern)}"
309+
end
310+
end
311+
312+
puts port_line
313+
314+
unless port.scripts.empty?
315+
puts
316+
317+
indent do
318+
highlight_scripts(port,pattern)
319+
end
320+
end
321+
end
322+
323+
#
324+
# Prints the nmap scripts with the pattern highlighted in the output.
325+
#
326+
# @param [::Nmap::XML::Scripts] has_scripts
327+
# The nmap XML object that has scripts.
328+
#
329+
# @param [String] pattern
330+
# The text pattern to highlight in the output.
331+
#
332+
def highlight_scripts(has_scripts,pattern)
333+
has_scripts.scripts.each_value do |script|
334+
highlight_script(script,pattern)
335+
puts
336+
end
337+
end
338+
339+
#
340+
# Prints the nmap script with the pattern highlighted in the output.
341+
#
342+
# @param [::Nmap::XML::Script] script
343+
# The nmap XML script object to print.
344+
#
345+
# @param [String] pattern
346+
# The text pattern to highlight in the output.
347+
#
348+
def highlight_script(script,pattern)
349+
puts "#{highlight(script.id,pattern)}:"
350+
351+
indent do
352+
script.output.strip.each_line do |line|
353+
puts highlight(line,pattern)
354+
end
355+
end
356+
end
357+
358+
#
359+
# Highlights the pattern in the text.
360+
#
361+
# @param [String] text
362+
# The text to modify.
363+
#
364+
# @param [String] pattern
365+
# The pattern to highlight.
366+
#
367+
# @return [String]
368+
# The modified text.
369+
#
370+
def highlight(text,pattern)
371+
text.to_s.gsub(pattern,colors.bold(colors.red(pattern)))
372+
end
373+
374+
end
375+
end
376+
end
377+
end
378+
end

0 commit comments

Comments
 (0)