antfarm 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. data/CHANGELOG +9 -0
  2. data/{README → README.rdoc} +39 -5
  3. data/bin/antfarm +37 -4
  4. data/db/migrate/010_create_dns_entries.rb +32 -0
  5. data/db/migrate/011_create_actions.rb +34 -0
  6. data/db/migrate/012_create_services.rb +36 -0
  7. data/db/migrate/013_create_operating_systems.rb +34 -0
  8. data/db/schema.rb +30 -22
  9. data/lib/antfarm.jar +0 -0
  10. data/lib/antfarm.rb +4 -0
  11. data/lib/antfarm/action.rb +29 -0
  12. data/lib/antfarm/dns_entry.rb +23 -0
  13. data/lib/antfarm/ip_interface.rb +38 -26
  14. data/lib/antfarm/ip_network.rb +3 -3
  15. data/lib/antfarm/layer2_interface.rb +16 -3
  16. data/lib/antfarm/layer3_network.rb +4 -4
  17. data/lib/antfarm/node.rb +6 -0
  18. data/lib/antfarm/operating_system.rb +25 -0
  19. data/lib/antfarm/service.rb +25 -0
  20. data/lib/console.rb +9 -0
  21. data/lib/cpscript.rb +70 -0
  22. data/lib/dbmanage.rb +34 -5
  23. data/lib/init/initializer.rb +25 -3
  24. data/lib/scparse.rb +14 -1
  25. data/lib/scripts/cisco/parse-pix-config.rb +5 -14
  26. data/lib/scripts/manipulate-dns.rb +87 -0
  27. data/lib/scripts/nmap/parse-xml.rb +147 -0
  28. data/lib/scripts/pcap/parse-pcap-file.rb +83 -21
  29. data/lib/scripts/viz/display-networks.rb +16 -25
  30. data/lib/scripts/viz/display-traffic.rb +111 -0
  31. data/lib/scripts/viz/dump-graphml.rb +1 -1
  32. data/lib/version.rb +5 -0
  33. data/rails/app/controllers/actions_controller.rb +5 -0
  34. data/rails/app/controllers/dns_entries_controller.rb +4 -0
  35. data/rails/app/controllers/layer3_interfaces_controller.rb +2 -2
  36. data/rails/app/controllers/nodes_controller.rb +4 -4
  37. data/rails/app/controllers/operating_systems_controller.rb +5 -0
  38. data/rails/app/controllers/services_controller.rb +5 -0
  39. data/rails/app/controllers/traffic_controller.rb +1 -0
  40. data/rails/app/views/layouts/application.html.erb +35 -0
  41. data/rails/public/stylesheets/site.css +11 -0
  42. metadata +26 -35
  43. data/lib/scripts/load-route.rb +0 -79
  44. data/lib/scripts/load-router-nomac.rb +0 -60
  45. data/lib/scripts/load-router.rb +0 -59
  46. data/lib/scripts/nmap/parse-xml-results.rb +0 -240
  47. data/lib/scripts/route.rb +0 -89
  48. data/lib/scripts/tethereal/load-arp.rb +0 -67
  49. data/lib/scripts/tethereal/load-ip.rb +0 -65
  50. data/rails/app/helpers/application_helper.rb +0 -3
  51. data/rails/app/helpers/ethernet_interfaces_helper.rb +0 -2
  52. data/rails/app/helpers/ip_interfaces_helper.rb +0 -2
  53. data/rails/app/helpers/ip_networks_helper.rb +0 -2
  54. data/rails/app/helpers/layer2_interfaces_helper.rb +0 -2
  55. data/rails/app/helpers/layer3_interfaces_helper.rb +0 -5
  56. data/rails/app/helpers/layer3_networks_helper.rb +0 -2
  57. data/rails/app/helpers/nodes_helper.rb +0 -2
  58. data/rails/app/helpers/private_networks_helper.rb +0 -2
  59. data/rails/app/helpers/traffic_helper.rb +0 -2
  60. data/rails/app/views/layouts/ethernet_interfaces.html.erb +0 -15
  61. data/rails/app/views/layouts/ip_interfaces.html.erb +0 -15
  62. data/rails/app/views/layouts/ip_networks.html.erb +0 -15
  63. data/rails/app/views/layouts/layer2_interfaces.html.erb +0 -15
  64. data/rails/app/views/layouts/layer3_interfaces.html.erb +0 -15
  65. data/rails/app/views/layouts/layer3_networks.html.erb +0 -15
  66. data/rails/app/views/layouts/nodes.html.erb +0 -15
  67. data/rails/app/views/layouts/private_networks.html.erb +0 -15
  68. data/rails/app/views/layouts/traffic.html.erb +0 -15
  69. data/rails/public/000-index.html +0 -277
@@ -87,7 +87,7 @@ class IpInterface < ActiveRecord::Base
87
87
  super(@ip_addr.to_s)
88
88
  end
89
89
 
90
- validates_presence_of :address
90
+ validates_presence_of :address
91
91
 
92
92
  # Validate data for requirements before saving interface to the database.
93
93
  #
@@ -95,9 +95,20 @@ class IpInterface < ActiveRecord::Base
95
95
  # on anything saved to the database at any time, including a create and an update.
96
96
  def validate #:nodoc:
97
97
  # Don't save the interface if it's a loopback address.
98
- unless !@ip_addr.loopback_address?
98
+ if @ip_addr.loopback_address?
99
99
  errors.add(:address, "loopback address not allowed")
100
100
  end
101
+
102
+ # If the address is public and it already exists in the database, don't create
103
+ # a new one but still create a new IP Network just in case the data given for
104
+ # this address includes more detailed information about its network.
105
+ unless @ip_addr.private_address?
106
+ interfaces = IpInterface.find :all, :conditions => { :address => address }
107
+ if interfaces && interfaces.length > 0
108
+ create_ip_network
109
+ errors.add(:address, 'address already exists, but a new IP Network was created')
110
+ end
111
+ end
101
112
  end
102
113
 
103
114
  # This is for ActiveScaffold
@@ -122,30 +133,7 @@ class IpInterface < ActiveRecord::Base
122
133
  if @layer3_network
123
134
  layer3_interface.layer3_network = @layer3_network
124
135
  else
125
- # Check to see if a network exists that contains this address.
126
- # If not, create a small one that does.
127
- layer3_network = Layer3Network.network_containing(@ip_addr.to_cidr_string)
128
- unless layer3_network
129
- network = @ip_addr.clone
130
- if network == network.network
131
- network.netmask = network.netmask << 3
132
- end
133
-
134
- ip_network = IpNetwork.new :address => network.to_cidr_string
135
- ip_network.layer3_network_protocol = @layer3_network_protocol if @layer3_network_protocol
136
- if ip_network.save
137
- logger.info("IpInterface: Created IP Network")
138
- else
139
- logger.warn("IpInterface: Errors occured while creating IP Network")
140
- ip_network.errors.each_full do |msg|
141
- logger.warn(msg)
142
- end
143
- end
144
-
145
- layer3_network = ip_network.layer3_network
146
- end
147
-
148
- layer3_interface.layer3_network = layer3_network
136
+ layer3_interface.layer3_network = create_ip_network
149
137
  end
150
138
 
151
139
  if @layer2_interface
@@ -174,4 +162,28 @@ class IpInterface < ActiveRecord::Base
174
162
  self.layer3_interface = layer3_interface
175
163
  end
176
164
  end
165
+
166
+ def create_ip_network
167
+ # Check to see if a network exists that contains this address.
168
+ # If not, create a small one that does.
169
+ layer3_network = Layer3Network.network_containing(@ip_addr.to_cidr_string)
170
+ unless layer3_network
171
+ network = @ip_addr.clone
172
+ if network == network.network
173
+ network.netmask = network.netmask << 3
174
+ end
175
+ ip_network = IpNetwork.new :address => network.to_cidr_string
176
+ ip_network.layer3_network_protocol = @layer3_network_protocol if @layer3_network_protocol
177
+ if ip_network.save
178
+ logger.info("IpInterface: Created IP Network")
179
+ else
180
+ logger.warn("IpInterface: Errors occured while creating IP Network")
181
+ ip_network.errors.each_full do |msg|
182
+ logger.warn(msg)
183
+ end
184
+ end
185
+ layer3_network = ip_network.layer3_network
186
+ end
187
+ return layer3_network
188
+ end
177
189
  end
@@ -28,9 +28,9 @@ class IpNetwork < ActiveRecord::Base
28
28
  belongs_to :layer3_network, :foreign_key => "id"
29
29
  belongs_to :private_network
30
30
 
31
- before_validation :set_private_address
32
31
  before_create :create_layer3_network
33
- after_create :merge_layer3_networks
32
+ before_create :set_private_address
33
+ after_create :merge_layer3_networks
34
34
 
35
35
  # Protocol of the layer 3 network automatically
36
36
  # created for this IP network.
@@ -75,7 +75,7 @@ class IpNetwork < ActiveRecord::Base
75
75
  def set_private_address
76
76
  self.private = @ip_net.private_address?
77
77
  # TODO: Create private network objects.
78
- return true
78
+ return # if we don't do this, then a false is returned and the save fails
79
79
  end
80
80
 
81
81
  def create_layer3_network
@@ -75,9 +75,22 @@ class Layer2Interface < ActiveRecord::Base
75
75
 
76
76
  def create_node
77
77
  unless self.node
78
- node = Node.new :certainty_factor => 0.75
79
- node.name = @node_name if @node_name
80
- node.device_type = @node_device_type if @node_device_type
78
+ if @node_name
79
+ nodes = Node.find(:all, :conditions => { :name => @node_name })
80
+ case nodes.length
81
+ when 0
82
+ node = Node.new :certainty_factor => 0.75
83
+ node.name = @node_name
84
+ when 1
85
+ node = nodes.first
86
+ else
87
+ node = nodes.first
88
+ #TODO: do something here with certainty factor?
89
+ end
90
+ else
91
+ node = Node.new :certainty_factor => 0.75
92
+ end
93
+ node.device_type = @node_device_type
81
94
  if node.save
82
95
  logger.info("Layer2Interface: Created Node")
83
96
  else
@@ -47,13 +47,13 @@ class Layer3Network < ActiveRecord::Base
47
47
 
48
48
  merge_certainty_factor = Antfarm.clamp(merge_certainty_factor)
49
49
 
50
- l3_ifs = network.layer3_interfaces
51
- l3_ifs << sub_network.layer3_interfaces
52
- l3_ifs.flatten!
50
+ network.layer3_interfaces << sub_network.layer3_interfaces
51
+ network.layer3_interfaces.flatten!
52
+ network.layer3_interfaces.uniq!
53
53
 
54
54
  # TODO: update network's certainty factor using sub_network's certainty factor.
55
55
 
56
- network.update_attributes :layer3_interfaces => l3_ifs
56
+ network.save false
57
57
 
58
58
  # Because of :dependent => :destroy above, calling destroy
59
59
  # here will also cause destroy to be called on ip_network
@@ -26,6 +26,8 @@
26
26
  class Node < ActiveRecord::Base
27
27
  has_many :layer2_interfaces
28
28
  has_many :layer3_interfaces, :through => :layer2_interfaces
29
+ has_many :services
30
+ has_one :operating_system
29
31
 
30
32
  before_save :clamp_certainty_factor
31
33
 
@@ -67,6 +69,10 @@ class Node < ActiveRecord::Base
67
69
  end
68
70
  end
69
71
 
72
+ def to_label
73
+ return self
74
+ end
75
+
70
76
  #######
71
77
  private
72
78
  #######
@@ -0,0 +1,25 @@
1
+ # Copyright (2008) Sandia Corporation.
2
+ # Under the terms of Contract DE-AC04-94AL85000 with Sandia Corporation,
3
+ # the U.S. Government retains certain rights in this software.
4
+ #
5
+ # Original Author: Bryan T. Richardson, Sandia National Laboratories <btricha@sandia.gov>
6
+ # Derived From: code written by Michael Berg <mjberg@sandia.gov>
7
+ #
8
+ # This library is free software; you can redistribute it and/or modify it
9
+ # under the terms of the GNU Lesser General Public License as published by
10
+ # the Free Software Foundation; either version 2.1 of the License, or (at
11
+ # your option) any later version.
12
+ #
13
+ # This library is distributed in the hope that it will be useful, but WITHOUT
14
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
15
+ # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
16
+ # details.
17
+ #
18
+ # You should have received a copy of the GNU Lesser General Public License
19
+ # along with this library; if not, write to the Free Software Foundation, Inc.,
20
+ # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
21
+
22
+ class OperatingSystem < ActiveRecord::Base
23
+ belongs_to :action
24
+ belongs_to :node
25
+ end
@@ -0,0 +1,25 @@
1
+ # Copyright (2008) Sandia Corporation.
2
+ # Under the terms of Contract DE-AC04-94AL85000 with Sandia Corporation,
3
+ # the U.S. Government retains certain rights in this software.
4
+ #
5
+ # Original Author: Bryan T. Richardson, Sandia National Laboratories <btricha@sandia.gov>
6
+ # Derived From: code written by Michael Berg <mjberg@sandia.gov>
7
+ #
8
+ # This library is free software; you can redistribute it and/or modify it
9
+ # under the terms of the GNU Lesser General Public License as published by
10
+ # the Free Software Foundation; either version 2.1 of the License, or (at
11
+ # your option) any later version.
12
+ #
13
+ # This library is distributed in the hope that it will be useful, but WITHOUT
14
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
15
+ # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
16
+ # details.
17
+ #
18
+ # You should have received a copy of the GNU Lesser General Public License
19
+ # along with this library; if not, write to the Free Software Foundation, Inc.,
20
+ # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
21
+
22
+ class Service < ActiveRecord::Base
23
+ belongs_to :action
24
+ belongs_to :node
25
+ end
@@ -0,0 +1,9 @@
1
+ options = { :irb => 'irb' }
2
+
3
+ ENV['ANTFARM_ENV'] = ARGV.shift
4
+ libs = " -r irb/completion"
5
+ libs << %( -r "#{ANTFARM_ROOT}/config/environment")
6
+
7
+ puts "Loading #{ENV['ANTFARM_ENV']} environment"
8
+
9
+ exec "#{options[:irb]} #{libs} --simple-prompt"
@@ -0,0 +1,70 @@
1
+ # Copyright (2008) Sandia Corporation.
2
+ # Under the terms of Contract DE-AC04-94AL85000 with Sandia Corporation,
3
+ # the U.S. Government retains certain rights in this software.
4
+ #
5
+ # Original Author: Bryan T. Richardson, Sandia National Laboratories <btricha@sandia.gov>
6
+ #
7
+ # This library is free software; you can redistribute it and/or modify it
8
+ # under the terms of the GNU Lesser General Public License as published by
9
+ # the Free Software Foundation; either version 2.1 of the License, or (at
10
+ # your option) any later version.
11
+ #
12
+ # This library is distributed in the hope that it will be useful, but WITHOUT
13
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
14
+ # FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
15
+ # details.
16
+ #
17
+ # You should have received a copy of the GNU Lesser General Public License
18
+ # along with this library; if not, write to the Free Software Foundation, Inc.,
19
+ # 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
20
+
21
+ require 'fileutils'
22
+
23
+ module Antfarm
24
+
25
+ # Extends SCParse::Command so it can be considered as a command.
26
+ class CPScript < SCParse::Command
27
+ def initialize
28
+ # set the command name to 'cp'
29
+ super('cp-script')
30
+ end
31
+
32
+ def execute(args)
33
+ super(args)
34
+
35
+ if defined?(USER_DIR)
36
+ script = args.pop + '.rb'
37
+ parents = args.join('/')
38
+ location = File.expand_path("#{ANTFARM_ROOT}/lib/scripts/#{parents}/#{script}")
39
+ if File.exists?(location)
40
+ FileUtils.makedirs("#{USER_DIR}/scripts/#{parents}")
41
+ FileUtils.cp(location, "#{USER_DIR}/scripts/#{parents}/")
42
+ if parents.empty?
43
+ puts "The script #{script} has been copied to #{USER_DIR}/scripts/"
44
+ else
45
+ puts "The script #{script} has been copied to #{USER_DIR}/scripts/#{parents}/"
46
+ end
47
+ else
48
+ if parents.empty?
49
+ puts "The script #{script} doesn't seem to exist. Please try again."
50
+ else
51
+ puts "The script #{parents}/#{script} doesn't seem to exist. Please try again."
52
+ end
53
+ end
54
+ else
55
+ puts "No custom user directory exists. Please run 'antfarm db --initialize' first."
56
+ end
57
+ end
58
+
59
+ def show_help
60
+ super
61
+
62
+ puts "This command is used to copy scripts available in the core ANTFARM package to"
63
+ puts "your user directory. This is useful for utilizing existing core ANTFARM scripts"
64
+ puts "as a basis for creating your own custom scripts."
65
+ puts
66
+ puts "As arguments to this command, specify the script you want to copy to your user"
67
+ puts "directory. For example: antfarm cp-script cisco parse-arp"
68
+ end
69
+ end
70
+ end
@@ -34,6 +34,7 @@ module Antfarm
34
34
  @opts.migrate = false
35
35
  @opts.remove = false
36
36
  @opts.reset = false
37
+ @opts.console = false
37
38
 
38
39
  @options = OptionParser.new do |opts|
39
40
  opts.on('--initialize', "Initialize user's environment") do
@@ -47,6 +48,7 @@ module Antfarm
47
48
  @opts.migrate = true
48
49
  end
49
50
  opts.on('--reset', "Reset tables in database and clear the log file for the given environment") { @opts.reset = true }
51
+ opts.on('--console', "Start up an SQLite3 console using the database for the given environment") { @opts.console = true }
50
52
  end
51
53
  end
52
54
 
@@ -63,6 +65,8 @@ module Antfarm
63
65
  db_migrate
64
66
  elsif @opts.reset
65
67
  db_reset
68
+ elsif @opts.console
69
+ db_console
66
70
  end
67
71
  end
68
72
 
@@ -101,6 +105,10 @@ module Antfarm
101
105
  Find.find(Antfarm.log_dir_to_use) do |path|
102
106
  `rm #{path}` unless File.basename(path) == 'log'
103
107
  end
108
+
109
+ Find.find(Antfarm.tmp_dir_to_use) do |path|
110
+ `rm #{path}` unless File.basename(path) == 'tmp'
111
+ end
104
112
  end
105
113
 
106
114
  # Creates a new database and schema file. The location of the newly created
@@ -108,13 +116,22 @@ module Antfarm
108
116
  # configuration hash, and is based on the current ANTFARM environment.
109
117
  def db_migrate
110
118
  if File.exists?(File.expand_path("#{ANTFARM_ROOT}/db/schema.rb"))
111
- load(File.expand_path("#{ANTFARM_ROOT}/db/schema.rb"))
119
+ begin
120
+ load(File.expand_path("#{ANTFARM_ROOT}/db/schema.rb"))
121
+ rescue PGError
122
+ puts "Looks like you are using the PostgreSQL database and you haven't yet created a database for this environment."
123
+ puts "Please execute 'psql #{ANTFARM_ENV}' to create the database, then try running 'antfarm db --migrate' again."
124
+ end
112
125
  else
113
126
  puts "A schema file did not exist. Running migrations instead."
114
-
115
- ActiveRecord::Migration.verbose = true
116
- ActiveRecord::Migrator.migrate(ANTFARM_ROOT + "/db/migrate/", nil)
117
- db_schema_dump if ActiveRecord::Base.schema_format == :ruby
127
+ begin
128
+ ActiveRecord::Migration.verbose = true
129
+ ActiveRecord::Migrator.migrate(ANTFARM_ROOT + "/db/migrate/", nil)
130
+ db_schema_dump if ActiveRecord::Base.schema_format == :ruby
131
+ rescue PGError
132
+ puts "Looks like you are using the PostgreSQL database and you haven't yet created a database for this environment."
133
+ puts "Please execute 'psql #{ANTFARM_ENV}' to create the database, then try running 'antfarm db --migrate' again."
134
+ end
118
135
  end
119
136
  end
120
137
 
@@ -123,6 +140,18 @@ module Antfarm
123
140
  db_migrate
124
141
  end
125
142
 
143
+ def db_console
144
+ if (defined? USER_DIR) && File.exists?("#{USER_DIR}/config/defaults.yml")
145
+ config = YAML::load(IO.read("#{USER_DIR}/config/defaults.yml"))
146
+ end
147
+ puts "Loading #{ANTFARM_ENV} environment"
148
+ if config && config[ANTFARM_ENV] && config[ANTFARM_ENV]['adapter'] == 'postgresql'
149
+ exec "psql #{ANTFARM_ENV}"
150
+ else
151
+ exec "sqlite3 #{Antfarm.db_file_to_use}"
152
+ end
153
+ end
154
+
126
155
  def db_schema_dump
127
156
  require 'active_record/schema_dumper'
128
157
 
@@ -20,6 +20,7 @@
20
20
  #
21
21
  # This script is modeled after the Rails initializer class.
22
22
 
23
+ require 'yaml'
23
24
  require 'rubygems'
24
25
  require 'active_record'
25
26
 
@@ -61,10 +62,31 @@ module Antfarm
61
62
  require 'antfarm'
62
63
  end
63
64
 
64
- # Currently, sqlite3 databases are the only ones supported. The name of the ANTFARM environment
65
- # (which defaults to 'antfarm') is the name used for the database file and the log file.
65
+ # Currently, SQLite3 and PostgreSQL databases are the only ones supported.
66
+ # The name of the ANTFARM environment (which defaults to 'antfarm') is the
67
+ # name used for the database file and the log file.
66
68
  def initialize_database
67
- config = { ANTFARM_ENV => { 'adapter' => 'sqlite3', 'database' => Antfarm.db_file_to_use } }
69
+ if (defined? USER_DIR) && File.exists?("#{USER_DIR}/config/defaults.yml")
70
+ config = YAML::load(IO.read("#{USER_DIR}/config/defaults.yml"))
71
+ end
72
+ # Database setup based on adapter specified
73
+ if config && config[ANTFARM_ENV] && config[ANTFARM_ENV].has_key?('adapter')
74
+ if config[ANTFARM_ENV]['adapter'] == 'sqlite3'
75
+ config[ANTFARM_ENV]['database'] = Antfarm.db_file_to_use
76
+ elsif config[ANTFARM_ENV]['adapter'] == 'postgresql'
77
+ config[ANTFARM_ENV]['database'] = ANTFARM_ENV
78
+ else
79
+ # If adapter specified isn't one of sqlite3 or postgresql,
80
+ # default to SQLite3 database configuration.
81
+ config = nil
82
+ end
83
+ else
84
+ # If the current environment configuration doesn't specify a
85
+ # database adapter, default to SQLite3 database configuration.
86
+ config = nil
87
+ end
88
+ # Default to SQLite3 database configuration
89
+ config ||= { ANTFARM_ENV => { 'adapter' => 'sqlite3', 'database' => Antfarm.db_file_to_use } }
68
90
  ActiveRecord::Base.configurations = config
69
91
  ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[ANTFARM_ENV])
70
92
  end
@@ -66,7 +66,7 @@ module SCParse
66
66
  end
67
67
 
68
68
  def add_command(command)
69
- @commands = CommandHash.new unless @commands
69
+ @commands ||= CommandHash.new
70
70
  @commands[command.name] = command
71
71
  command.parent = self
72
72
  end
@@ -90,6 +90,12 @@ module SCParse
90
90
  return parents
91
91
  end
92
92
 
93
+ # Does this command have a given subcommand?
94
+ # Note: returns the command or nil, not true/false.
95
+ def has_command?(command)
96
+ return @commands[command]
97
+ end
98
+
93
99
  # Does this command have any subcommands it's a parent to?
94
100
  def has_commands?
95
101
  return @commands.nil? ? false : true
@@ -268,6 +274,13 @@ module SCParse
268
274
  @main.add_command(command)
269
275
  end
270
276
 
277
+ # Does this application have a command with
278
+ # the given name?
279
+ # Note: returns command or nil, not true/false.
280
+ def has_command?(command)
281
+ @main.has_command?(command)
282
+ end
283
+
271
284
  # Returns any options in the main command object.
272
285
  def options
273
286
  @main.options