diff --git a/README.md b/README.md
index 6bca5ca..0f1e769 100644
--- a/README.md
+++ b/README.md
@@ -43,6 +43,7 @@ Commands:
grep
help
import
+ new
print
scan
```
@@ -95,6 +96,18 @@ Convert an nmap XML scan file to JSON:
$ ronin-nmap convert scan.xml scan.json
```
+Generate a new nmap scanner Ruby script:
+
+```shell
+$ ronin-nmap new scanner.rb --ports 22,80,443,8000-9000 --target example.com
+```
+
+Generate a new nmap XML parser script:
+
+```shell
+$ ronin-nmap new parser.rb --parser --xml-file path/to/nmap.xml --printing
+```
+
## Examples
Performing an `nmap` scan and returning the parsed nmap XML data:
diff --git a/data/templates/script.rb.erb b/data/templates/script.rb.erb
new file mode 100644
index 0000000..fc154b9
--- /dev/null
+++ b/data/templates/script.rb.erb
@@ -0,0 +1,56 @@
+#!/usr/bin/env ruby
+
+require 'ronin/nmap'
+
+<%- if @script_type == :parser -%>
+<%- if @xml_file -%>
+xml = Ronin::Nmap.parse(<%= @xml_file.inspect %>)
+<%- else -%>
+xml = Ronin::Nmap.parse(ARGV[0])
+<%- end -%>
+<%- else -%>
+xml = Ronin::Nmap.scan(
+<%- case @targets.length -%>
+<%- when 0 -%>
+ ARGV[0],
+<%- when 1 -%>
+ <%= @targets[0].inspect %>,
+<%- else -%>
+ <%= @targets.inspect %>,
+<%- end -%>
+<%- if @syn_scan -%>
+ syn_scan: true,
+<%- end -%>
+<%- if @ports -%>
+ ports: <%= @ports.inspect %>,
+<%- else -%>
+ # ports: [22, 80, 443, 8000..9000],
+<%- end -%>
+<%- if @xml_file -%>
+ xml_file: <%= @xml_file.inspect %>
+<%- else -%>
+ # xml_file: "path/to/file.xml"
+<%- end -%>
+)
+<%- end -%>
+<% if @features[:printing] -%>
+
+xml.each_host do |host|
+ puts "[ #{host.ip} ]"
+
+ host.each_port do |port|
+ puts " #{port.number}/#{port.protocol}\t#{port.state}\t#{port.service}"
+
+ port.scripts.each do |id,script|
+ puts " [ #{id} ]"
+
+ script.output.each_line { |line| puts " #{line}" }
+ end
+ end
+end
+<%- end -%>
+<%- if @features[:import] -%>
+
+Ronin::DB.connect
+Ronin::Nmap::Importer.import(xml)
+<%- end -%>
diff --git a/gemspec.yml b/gemspec.yml
index 9176c51..64c5f94 100644
--- a/gemspec.yml
+++ b/gemspec.yml
@@ -26,6 +26,7 @@ generated_files:
- man/ronin-nmap-dump.1
- man/ronin-nmap-grep.1
- man/ronin-nmap-import.1
+ - man/ronin-nmap-new.1
- man/ronin-nmap-print.1
- man/ronin-nmap-scan.1
diff --git a/lib/ronin/nmap/cli/commands/new.rb b/lib/ronin/nmap/cli/commands/new.rb
new file mode 100644
index 0000000..6e0fddb
--- /dev/null
+++ b/lib/ronin/nmap/cli/commands/new.rb
@@ -0,0 +1,217 @@
+# frozen_string_literal: true
+#
+# ronin-nmap - A Ruby library for automating nmap and importing nmap scans.
+#
+# Copyright (c) 2023-2024 Hal Brodigan (postmodern.mod3@gmail.com)
+#
+# ronin-nmap is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Lesser General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# ronin-nmap is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with ronin-nmap. If not, see .
+#
+
+require 'ronin/nmap/cli/command'
+require 'ronin/nmap/root'
+
+require 'ronin/core/cli/generator'
+
+module Ronin
+ module Nmap
+ class CLI
+ module Commands
+ #
+ # Generates a new nmap ruby script.
+ #
+ # ## Usage
+ #
+ # ronin-nmap new [options] FILE
+ #
+ # ## Options
+ #
+ # --parser Generate a nmap XML parser script
+ # --scanner Generate a nmap scanner script
+ # --printing Adds additional printing of the nmap scan data
+ # --import Also import the nmap XML scan data
+ # --xml-file XML_FILE Sets the XML file to write to or parse
+ # -p {PORT | [PORT1]-[PORT2]},... Sets the port range to scan
+ # --ports
+ # --target TARGET Sets the targets to scan (Defaults: ARGV[0])
+ # -h, --help Print help information
+ #
+ # ## Arguments
+ #
+ # FILE The path to the new nmap ruby script.
+ #
+ # ## Examples
+ #
+ # ronin-nmap new scanner.rb --ports 22,80,443,8000-9000 --target example.com
+ # ronin-nmap new parser.rb --parser --xml-file path/to/nmap.xml --printing
+ #
+ class New < Command
+
+ include Core::CLI::Generator
+
+ template_dir File.join(ROOT,'data','templates')
+
+ usage '[options] FILE'
+
+ option :parser, desc: 'Generate a nmap XML parser script' do
+ @script_type = :parser
+ end
+
+ option :scanner, desc: 'Generate a nmap scanner script' do
+ @script_type = :scanner
+ end
+
+ option :printing, desc: 'Adds additional printing of the nmap scan data' do
+ @features[:printing] = true
+ end
+
+ option :import, desc: 'Also import the nmap XML scan data' do
+ @features[:import] = true
+ end
+
+ option :xml_file, value: {
+ type: String,
+ usage: 'XML_FILE'
+ },
+ desc: 'Sets the XML file to write to or parse' do |file|
+ @xml_file = file
+ end
+
+ option :ports, short: '-p',
+ value: {
+ type: String,
+ usage: '{PORT | [PORT1]-[PORT2]},...'
+ },
+ desc: 'Sets the port range to scan' do |ports|
+ @ports = parse_port_range(ports)
+ rescue ArgumentError => error
+ raise(OptionParser::InvalidArgument,error.message)
+ end
+
+ option :target, value: {
+ type: String,
+ usage: 'TARGET'
+ },
+ desc: 'Sets the targets to scan (Defaults: ARGV[0])' do |target|
+ @targets << target
+ end
+
+ argument :path, desc: 'The path to the new nmap ruby script'
+
+ description 'Generates a new nmap ruby script'
+
+ man_page 'ronin-nmap-new.1'
+
+ examples [
+ "scanner.rb --ports 22,80,443,8000-9000 --target example.com",
+ "parser.rb --parser --xml-file path/to/nmap.xml --printing"
+ ]
+
+ # The script type.
+ #
+ # @return [:scan, :parse]
+ attr_reader :script_type
+
+ # The optioanl XML file to write to or parse.
+ #
+ # @return [String, nil]
+ attr_reader :xml_file
+
+ # The optional ports to scan.
+ #
+ # @return [Array, "-", nil]
+ attr_reader :ports
+
+ # The targets to scan.
+ #
+ # @return [Array]
+ attr_reader :targets
+
+ # Additional features.
+ #
+ # @return [Hash{Symbol => Boolean}]
+ attr_reader :features
+
+ #
+ # Initializes the `ronin-nmap new` command.
+ #
+ # @param [Hash{Symbol => Object}] kwargs
+ # Additional keyword arguments for the command.
+ #
+ def initialize(**kwargs)
+ super(**kwargs)
+
+ @script_type = :scanner
+ @targets = []
+ @features = {}
+ end
+
+ #
+ # Runs the `ronin-nmap new` command.
+ #
+ # @param [String] file
+ # The path to the new nmap ruby script.
+ #
+ def run(file)
+ @directory = File.dirname(file)
+
+ mkdir @directory unless File.directory?(@directory)
+
+ erb "script.rb.erb", file
+ chmod '+x', file
+ end
+
+ #
+ # Parses a port range.
+ #
+ # @param [String] ports
+ # The port range to parse.
+ #
+ # @return [Array, "-"]
+ # The parsed port range.
+ #
+ # @raise [ArgumentError]
+ # An invalid port range was given.
+ #
+ def parse_port_range(ports)
+ case ports
+ when '-' then '-'
+ else
+ ports.split(',').map do |port|
+ case port
+ when /\A\d+-\d+\z/
+ start, stop = port.split('-',2)
+
+ (start.to_i..stop.to_i)
+ when /\A\d+-\z/
+ start = port.chomp('-')
+
+ (start.to_i..)
+ when /\A-\d+\z/
+ stop = port[1..]
+
+ (..stop.to_i)
+ when /\A\d+\z/
+ port.to_i
+ else
+ raise(ArgumentError,"invalid port range: #{ports.inspect}")
+ end
+ end
+ end
+ end
+
+ end
+ end
+ end
+ end
+end
diff --git a/man/ronin-nmap-new.1.md b/man/ronin-nmap-new.1.md
new file mode 100644
index 0000000..99cda9e
--- /dev/null
+++ b/man/ronin-nmap-new.1.md
@@ -0,0 +1,70 @@
+# ronin-nmap-new 1 "2023-03-01" Ronin Nmap "User Manuals"
+
+## NAME
+
+ronin-nmap-new - Generates a new nmap ruby script
+
+## SYNOPSIS
+
+`ronin-nmap new` [options] *FILE*
+
+## DESCRIPTION
+
+Generates a new nmap scanner or parser Ruby script that uses the `ronin-nmap`
+library.
+
+## ARGUMENTS
+
+*FILE*
+: The path to the new Ruby script to generate.
+
+## OPTIONS
+
+`--parser`
+: Generates a new nmap XML parser Ruby script.
+
+`--scanner`
+: Generates a new nmap scanner Ruby script.
+
+`--printing`
+: Adds additional code to the Ruby script that prints the nmap XML scan data.
+ Is compatible with both `--parser` and `--scanner`.
+
+`--import`
+: Adds additional code to the Ruby script that imports the nmap XML scan data.
+ Is compatible with both `--parser` and `--scanner`.
+
+`--xml-file` *XML_FILE*
+: Parses or writes the scan results to the given XML File.
+ Is compatible with both `--parser` and `--scanner`.
+
+`-p`, `--port` {*PORT* \| \[*PORT1*\]-\[*PORT2*\]},...
+: Specifies the ports to scan. Not compatible with the `--parser` option.
+
+`--target` *TARGET*
+: Adds a target to scan. May be a host name, IP, IP CIDR range (ex:
+ `192.168.1.1/24`), or IP glob range (ex: `192.168.*.1-4`).
+ Not compatible with the `--parser` option.
+
+`-h`, `--help`
+: Print help information
+
+## EXAMPLES
+
+Generates a new nmap scanner Ruby script that scans `example.com`, ports 22, 80,
+443, and 8000 through 9000:
+
+ $ ronin-nmap new scanner.rb --ports 22,80,443,8000-9000 --target example.com
+
+Generates a new nmap XML parser script that parses `path/to/nmap.xml` and prints
+the scan information.
+
+ $ ronin-nmap new parser.rb --parser --xml-file path/to/nmap.xml --printing
+
+## AUTHOR
+
+Postmodern
+
+## SEE ALSO
+
+[ronin-nmap-scan](ronin-nmap-scan.1.md), [ronin-nmap-print](ronin-nmap-print.1.md), [ronin-nmap-import](ronin-nmap-import.1.md)
diff --git a/spec/cli/commands/new_spec.rb b/spec/cli/commands/new_spec.rb
new file mode 100644
index 0000000..a57d6fd
--- /dev/null
+++ b/spec/cli/commands/new_spec.rb
@@ -0,0 +1,509 @@
+require 'spec_helper'
+require 'ronin/nmap/cli/commands/new'
+
+require 'tmpdir'
+
+describe Ronin::Nmap::CLI::Commands::New do
+ describe "options" do
+ before { subject.option_parser.parse(argv) }
+
+ context "when given the '--parser' option" do
+ let(:argv) { %w[--parser] }
+
+ it "must set #script_type to :parser" do
+ expect(subject.script_type).to eq(:parser)
+ end
+ end
+
+ context "when given the '--scanner ' option" do
+ let(:argv) { %w[--parser --scanner] }
+
+ it "must set #script_type to :scanner" do
+ expect(subject.script_type).to eq(:scanner)
+ end
+ end
+
+ context "when given the '--printing' option" do
+ let(:argv) { %w[--printing] }
+
+ it "must set :printing in #features" do
+ expect(subject.features[:printing]).to be(true)
+ end
+ end
+
+ context "when given the '--import' option" do
+ let(:argv) { %w[--import] }
+
+ it "must set :import in #features" do
+ expect(subject.features[:import]).to be(true)
+ end
+ end
+
+ context "when given the '--xml-file FILE' option" do
+ let(:file) { 'path/to/nmap.xml' }
+ let(:argv) { ['--xml-file', file] }
+
+ it "must set #xml_file to the given FILE argument" do
+ expect(subject.xml_file).to eq(file)
+ end
+ end
+
+ context "when given the '--ports PORT,...' option" do
+ let(:ports) { [22, 80, 443] }
+ let(:argv) { ['--ports', "#{ports.join(',')}"] }
+
+ it "must set #ports to an Array of port Integers" do
+ expect(subject.ports).to eq(ports)
+ end
+ end
+
+ context "when given the '--ports PORT1-PORT2,...' option" do
+ let(:start_port1) { 1 }
+ let(:stop_port1) { 1024 }
+ let(:start_port2) { 8000 }
+ let(:stop_port2) { 9000 }
+
+ let(:argv) do
+ ['--ports', "#{start_port1}-#{stop_port1},#{start_port2}-#{stop_port2}"]
+ end
+
+ it "must set #ports to an Array of Ranges of Integers" do
+ expect(subject.ports).to eq(
+ [
+ (start_port1..stop_port1),
+ (start_port2..stop_port2)
+ ]
+ )
+ end
+ end
+
+ context "when given the '--ports PORT1,PORT2,PORT3-PORT4,...' option" do
+ let(:port1) { 80 }
+ let(:port2) { 443 }
+ let(:start_port1) { 1 }
+ let(:stop_port1) { 1024 }
+ let(:start_port2) { 8000 }
+ let(:stop_port2) { 9000 }
+
+ let(:argv) do
+ ['--ports', "#{port1},#{port2},#{start_port1}-#{stop_port1},#{start_port2}-#{stop_port2}"]
+ end
+
+ it "must set #ports to an Array of Integers and Ranges of Integers" do
+ expect(subject.ports).to eq(
+ [
+ port1,
+ port2,
+ (start_port1..stop_port1),
+ (start_port2..stop_port2)
+ ]
+ )
+ end
+ end
+
+ context "when given the '--ports -' option" do
+ let(:argv) { %w[--ports -] }
+
+ it "must set #ports to '-'" do
+ expect(subject.ports).to eq('-')
+ end
+ end
+
+ context "when given the '--target TARGET' option" do
+ let(:target1) { 'example.com' }
+ let(:target2) { '192.168.1.1' }
+ let(:argv) { ['--target', target1, '--target', target2] }
+
+ it "must append the target value to #targets" do
+ expect(subject.targets).to eq([target1, target2])
+ end
+ end
+ end
+
+ describe "#initialize" do
+ it "must default #script_type to :scanner" do
+ expect(subject.script_type).to eq(:scanner)
+ end
+
+ it "must initialize #targets to an empty Array" do
+ expect(subject.targets).to eq([])
+ end
+
+ it "must initialize #features to an empty Hash" do
+ expect(subject.features).to eq({})
+ end
+ end
+
+ describe "#run" do
+ let(:tempdir) { Dir.mktmpdir('test-ronin-nmap-new') }
+ let(:path) { File.join(tempdir,'test_script.rb') }
+
+ let(:argv) { [] }
+
+ before do
+ subject.option_parser.parse(argv)
+ subject.run(path)
+ end
+
+ it "must generate a new file containing a new `Ronin::Nmap.scan(...)`" do
+ expect(File.read(path)).to eq(
+ <<~RUBY
+ #!/usr/bin/env ruby
+
+ require 'ronin/nmap'
+
+ xml = Ronin::Nmap.scan(
+ ARGV[0],
+ # ports: [22, 80, 443, 8000..9000],
+ # xml_file: "path/to/file.xml"
+ )
+ RUBY
+ )
+ end
+
+ it "must make the file executable" do
+ expect(File.executable?(path)).to be(true)
+ end
+
+ context "when the parent directory does not exist yet" do
+ let(:path) { File.join(tempdir,'does_not_exist_yet','test_script.rb') }
+
+ it "must create the parent directory" do
+ expect(File.directory?(File.dirname(path))).to be(true)
+ end
+ end
+
+ context "when given the '--parser' option" do
+ let(:argv) { %w[--parser] }
+
+ it "must generate a Ruby script that calls `Ronin::Nmap.parse(...)` instead" do
+ expect(File.read(path)).to eq(
+ <<~RUBY
+ #!/usr/bin/env ruby
+
+ require 'ronin/nmap'
+
+ xml = Ronin::Nmap.parse(ARGV[0])
+ RUBY
+ )
+ end
+
+ context "and when given the '--xml-file FILE' option" do
+ let(:file) { 'path/to/nmap.xml' }
+ let(:argv) { super() + ['--xml-file', file] }
+
+ it "must include the given file path instead of `ARGV[0]`" do
+ expect(File.read(path)).to eq(
+ <<~RUBY
+ #!/usr/bin/env ruby
+
+ require 'ronin/nmap'
+
+ xml = Ronin::Nmap.parse(#{file.inspect})
+ RUBY
+ )
+ end
+ end
+
+ context "when given the '--printing' option" do
+ let(:argv) { super() + %w[--printing] }
+
+ it "must append additional code to print the nmap XML scan data" do
+ expect(File.read(path)).to eq(
+ <<~RUBY
+ #!/usr/bin/env ruby
+
+ require 'ronin/nmap'
+
+ xml = Ronin::Nmap.parse(ARGV[0])
+
+ xml.each_host do |host|
+ puts "[ \#{host.ip} ]"
+
+ host.each_port do |port|
+ puts " \#{port.number}/\#{port.protocol}\\t\#{port.state}\\t\#{port.service}"
+
+ port.scripts.each do |id,script|
+ puts " [ \#{id} ]"
+
+ script.output.each_line { |line| puts " \#{line}" }
+ end
+ end
+ end
+ RUBY
+ )
+ end
+ end
+
+ context "and when given the '--scanner' option" do
+ let(:argv) { super() + %w[--scanner] }
+
+ it "must generate a new file containing a new `Ronin::Nmap.scan(...)` instead" do
+ expect(File.read(path)).to eq(
+ <<~RUBY
+ #!/usr/bin/env ruby
+
+ require 'ronin/nmap'
+
+ xml = Ronin::Nmap.scan(
+ ARGV[0],
+ # ports: [22, 80, 443, 8000..9000],
+ # xml_file: "path/to/file.xml"
+ )
+ RUBY
+ )
+ end
+ end
+ end
+
+ context "when given the '--printing' option" do
+ let(:argv) { %w[--printing] }
+
+ it "must append additional code to print the nmap XML scan data" do
+ expect(File.read(path)).to eq(
+ <<~RUBY
+ #!/usr/bin/env ruby
+
+ require 'ronin/nmap'
+
+ xml = Ronin::Nmap.scan(
+ ARGV[0],
+ # ports: [22, 80, 443, 8000..9000],
+ # xml_file: "path/to/file.xml"
+ )
+
+ xml.each_host do |host|
+ puts "[ \#{host.ip} ]"
+
+ host.each_port do |port|
+ puts " \#{port.number}/\#{port.protocol}\\t\#{port.state}\\t\#{port.service}"
+
+ port.scripts.each do |id,script|
+ puts " [ \#{id} ]"
+
+ script.output.each_line { |line| puts " \#{line}" }
+ end
+ end
+ end
+ RUBY
+ )
+ end
+ end
+
+ context "when given the '--import' option" do
+ let(:argv) { %w[--import] }
+
+ it "must append additional code to print the nmap XML scan data" do
+ expect(File.read(path)).to eq(
+ <<~RUBY
+ #!/usr/bin/env ruby
+
+ require 'ronin/nmap'
+
+ xml = Ronin::Nmap.scan(
+ ARGV[0],
+ # ports: [22, 80, 443, 8000..9000],
+ # xml_file: "path/to/file.xml"
+ )
+
+ Ronin::DB.connect
+ Ronin::Nmap::Importer.import(xml)
+ RUBY
+ )
+ end
+ end
+
+ context "when given the '--xml-file FILE' option" do
+ let(:file) { 'path/to/nmap.xml' }
+ let(:argv) { ['--xml-file', file] }
+
+ it "must add an `xml_file:` keyword argument to `Ronin::Nmap.scan(...)` with the given file" do
+ expect(File.read(path)).to eq(
+ <<~RUBY
+ #!/usr/bin/env ruby
+
+ require 'ronin/nmap'
+
+ xml = Ronin::Nmap.scan(
+ ARGV[0],
+ # ports: [22, 80, 443, 8000..9000],
+ xml_file: #{file.inspect}
+ )
+ RUBY
+ )
+ end
+ end
+
+ context "when given the '--ports PORT,...' option" do
+ let(:ports) { [22, 80, 443] }
+ let(:argv) { ['--ports', "#{ports.join(',')}"] }
+
+ it "must add an `ports:` keyword argument to `Ronin::Nmap.scan(...)` with an Array of the given port numbers" do
+ expect(File.read(path)).to eq(
+ <<~RUBY
+ #!/usr/bin/env ruby
+
+ require 'ronin/nmap'
+
+ xml = Ronin::Nmap.scan(
+ ARGV[0],
+ ports: #{ports.inspect},
+ # xml_file: "path/to/file.xml"
+ )
+ RUBY
+ )
+ end
+ end
+
+ context "when given the '--ports PORT1-PORT2,...' option" do
+ let(:start_port1) { 1 }
+ let(:stop_port1) { 1024 }
+ let(:start_port2) { 8000 }
+ let(:stop_port2) { 9000 }
+ let(:ports) do
+ [
+ (start_port1..stop_port1),
+ (start_port2..stop_port2)
+ ]
+ end
+
+ let(:argv) do
+ ['--ports', "#{start_port1}-#{stop_port1},#{start_port2}-#{stop_port2}"]
+ end
+
+ it "must add an `ports:` keyword argument to `Ronin::Nmap.scan(...)` with an Array of the given port ranges" do
+ expect(File.read(path)).to eq(
+ <<~RUBY
+ #!/usr/bin/env ruby
+
+ require 'ronin/nmap'
+
+ xml = Ronin::Nmap.scan(
+ ARGV[0],
+ ports: #{ports.inspect},
+ # xml_file: "path/to/file.xml"
+ )
+ RUBY
+ )
+ end
+ end
+
+ context "when given the '--ports PORT1,PORT2,PORT3-PORT4,...' option" do
+ let(:port1) { 80 }
+ let(:port2) { 443 }
+ let(:start_port1) { 1 }
+ let(:stop_port1) { 1024 }
+ let(:start_port2) { 8000 }
+ let(:stop_port2) { 9000 }
+ let(:ports) do
+ [
+ port1,
+ port2,
+ (start_port1..stop_port1),
+ (start_port2..stop_port2)
+ ]
+ end
+
+ let(:argv) do
+ ['--ports', "#{port1},#{port2},#{start_port1}-#{stop_port1},#{start_port2}-#{stop_port2}"]
+ end
+
+ it "must add an `ports:` keyword argument to `Ronin::Nmap.scan(...)` with an Array of the given port numbers and ranges" do
+ expect(File.read(path)).to eq(
+ <<~RUBY
+ #!/usr/bin/env ruby
+
+ require 'ronin/nmap'
+
+ xml = Ronin::Nmap.scan(
+ ARGV[0],
+ ports: #{ports.inspect},
+ # xml_file: "path/to/file.xml"
+ )
+ RUBY
+ )
+ end
+ end
+
+ context "when given the '--ports -' option" do
+ let(:argv) { %w[--ports -] }
+
+ it "must add an `ports:` keyword argument to `Ronin::Nmap.scan(...)` with '-'" do
+ expect(File.read(path)).to eq(
+ <<~RUBY
+ #!/usr/bin/env ruby
+
+ require 'ronin/nmap'
+
+ xml = Ronin::Nmap.scan(
+ ARGV[0],
+ ports: "-",
+ # xml_file: "path/to/file.xml"
+ )
+ RUBY
+ )
+ end
+ end
+ end
+
+ describe "#parse_port_range" do
+ context "when given 'PORT,...'" do
+ let(:ports) { [22, 80, 443] }
+ let(:string) { ports.join(',') }
+
+ it "must parse the string into an Array of port Integers" do
+ expect(subject.parse_port_range(string)).to eq(ports)
+ end
+ end
+
+ context "when given 'PORT1-PORT2,...'" do
+ let(:start_port1) { 1 }
+ let(:stop_port1) { 1024 }
+ let(:start_port2) { 8000 }
+ let(:stop_port2) { 9000 }
+
+ let(:string) do
+ "#{start_port1}-#{stop_port1},#{start_port2}-#{stop_port2}"
+ end
+
+ it "must parse the string into an Array of Ranges of Integers" do
+ expect(subject.parse_port_range(string)).to eq(
+ [
+ (start_port1..stop_port1),
+ (start_port2..stop_port2)
+ ]
+ )
+ end
+ end
+
+ context "when given 'PORT1,PORT2,PORT3-PORT4,...'" do
+ let(:port1) { 80 }
+ let(:port2) { 443 }
+ let(:start_port1) { 1 }
+ let(:stop_port1) { 1024 }
+ let(:start_port2) { 8000 }
+ let(:stop_port2) { 9000 }
+
+ let(:string) do
+ "#{port1},#{port2},#{start_port1}-#{stop_port1},#{start_port2}-#{stop_port2}"
+ end
+
+ it "must parse the string into an Array of Integers and Ranges of Integers" do
+ expect(subject.parse_port_range(string)).to eq(
+ [
+ port1,
+ port2,
+ (start_port1..stop_port1),
+ (start_port2..stop_port2)
+ ]
+ )
+ end
+ end
+
+ context "when given '-'" do
+ it "must return '-'" do
+ expect(subject.parse_port_range('-')).to eq('-')
+ end
+ end
+ end
+end